For most other practical purposes... probably Racket.
There are a few other options I still keep in mind, including Gambit and Chicken.
(Actually, lately, the "Lisp" I'm using is Python. Python is usually tolerable, and the huge ecosystem and easy employability are nice in some ways. But Lisp technical sophistication and community signal:noise ratio tend to increase with the percentage of parentheses. Not because parentheses are magical, though they are-- but because the parentheses Lisps are built on better original foundations, and then disproportionately attract people on technical merits rather than marketability.)
Do you think that's a reasonable way to become a more confident and employable programmer?
They use Racket and are based on the book How to Design Programs. I think they're a great supplement to SICP, which is an excellent book you should stick with. The book The Little Schemer is also helpful for solidifying recursion principles. The Programming Languages courses (Parts A, B, and C) on Coursera are excellent two. Part A uses SML, and is another good source for recursion practice, Part B uses Racket, and Part C uses Ruby.
Lastly, are you going through SICP using Scheme or Racket? If not, I highly recommend doing so (in the case you are using the Python version of the book, which has been modified heavily from the original).
> New datatypes are normally created with the struct form, which is the topic of this chapter. The class-based object system, which we defer to Classes and Objects, offers an alternate mechanism for creating new datatypes, but even classes and objects are implemented in terms of structure types.
This sort of phrasing is sort of distressingly characteristic of contemporary discussions about object-oriented programming.
I can understand where it comes from - if you look through my comment history, you'll probably find plenty of examples of me running my mouth about Java or the overuse of OOP in Python - but I assure you there is a baby in all of that bathwater, and it doesn't need to be thrown out. That baby is dynamic dispatch, and it does let you do some things that can be a hassle without it.
Unfortunately, it tends to be poorly taught or poorly understood nowadays. This is not a "no true Scotsman" argument. I'm not saying no true hammer would be used to drive screws. I'm just observing that, if a carpenter was never taught about screwdrivers, it's no surprise they'll develop a lot of pent-up frustration about hammers, and perhaps even be a bit confused about what they're really for. It seemed to me that one of the core sections of the essay ikrubner linked above rested on a conflation of dynamic dispatch and dynamic typing. Which is a worrisome thing I've seen many people do. It's no wonder we don't use OOP well, living in an intellectual environment where its only truly distinguishing characteristic rarely even makes it into lists of its key features. So, perhaps it's even worse than just having only been given a hammer. We were only given a hammer, and were also never taught about nails.
Anyway, enough complaining. Let's take Racket's image drawing library as a positive example. It's implemented around an object-oriented interface. What this does is lets you write code against that interface, so that it is able to output graphics to any format that has a suitable implementation of that interface, without needing any special glue code or explicit branching logic to make it happen. I would argue that this is quite useful.
As a contrasting example, Haskell's typeclasses are superficially quite similar, but, since they use static dispatch, adding a new output format does tend to require some glue code - not much, but still more than zero - that's specific to each drawing routine you want to hook up to that new format.
There's a nice essay, https://www.sicpers.info/2018/03/why-inheritance-never-made-... , that serves as an excellent excoriation of the C++/Java/etc lineage of OOP. But one key thing needed to contextualize it is that OOP mechanisms like Racket's or CLOS aren't trying to do OOP in the same way that Java does, and therefore don't run afoul of the same shortcomings. In particular, they're not trying to turn it into a single golden hammer that solves all abstraction problems. Racket gets a lot more mileage out of its module mechanism, for example.
But IMHO the highest-value things you can do to become a better software developer are to simply practice software development of various kinds. Work on software development in a company, contribute to open source projects, do little personal side projects (that ideally you polish up with quality and documentation, so that other people can use/extend them, or sometimes move on because opportunity cost), etc. Experiment with styles, technical approaches, etc.
When you can, work on things you want to work on, and let your needs and curiosity there guide what you spend time learning and practicing.
When you really have to grind Leetcode for interview theatre reasons, it's not entirely without learning value, but it has very little to do with real-world software development. So don't waste all your time on Leetcode. Pass the interviews you want to pass, but otherwise spend your time on more substantial and rewarding things. IMHO.
As it stands, I just don't think I'm competent enough to get hired at a quality company. I've worked at a bad company in a different industry and quit because I was being taught to be a manager from hell, and it was beginning to take despite my contrary intentions.
I'm 30 and making a career change, so I'd like to start somewhere I'll pick up good methods. I'd also like to be able to recognize that kind of place. Both depend on a base-level of competence.
I am putting together a CRUD app for a purpose I'm excited about, but I just reeeally feel like I'm flying blind and keep getting stuck. Hence my pursuit of fundamentals.
I do have my eye on an open source project I'd like to contribute to once I have a little more confidence.
> I'd like to start somewhere I'll pick up good methods. I'd also like to be able to recognize that kind of place.
The Recurse Center is worth looking into. https://www.recurse.com/ When I studied SICP back in the 80s I had to do it on my own, but a community of fellow learners can make a real difference in what you get out of your studies and how much you keep at it.
Pretty much everyone gets overwhelmed with the bureaucracy, and people end up cargo-culting to various degrees. Don't get intimidated.
Once you find a good framework and coherent documentation for it, then one of the usual rules applies: getting good at something takes time, and it's important not to get discouraged.
If the framework community has a text chat that answers questions quickly, that can help.
Good luck, and have fun exploring.
By the way, are you aware of the SICP Python version?
When feeling more confident you can even try a go at something like doing a toy compiler.
Or if you prefer a book instead,
Also being forced to read the docs b/c there are not as many YouTube videos and stack overflow answers has been great to get over my fear of doing that in python.
Lastly not having a specific library requiring you to develop more of the code from scratch, has helped me think about how to tackle harder problems on my own.
Some helpful tips for a beginner in Racket.
1. The Racket discord is very helpful
2. If you are having trouble debugging an error in Racket, and it isn’t giving you a specific line number. Try racket -l errortrace -t my-program.rkt
3. Racket has a vscode plugin called Magic Racket
4. Drracket’s syntax highlighting leaves a lot to be desired, but you can change what gets highlighted to a high degree of granularity in it’s settings
(https://github.com/bendudson/py4cl/ to use python libs from CL!)
I went from racket to guile because I find guile more fun. For production things I would probably use racket (or even better Chez isbthat was an option), but guile is what I use to keep my soul.
Racket: From the people who brought you the quote: "the toplevel is hopeless".
There is good reason for it in the racket context, but if you want a lisp machine you should pick something else.
Racket has "batteries included", so it was just connecting the output of one library with the input of the next library. (And reading a few times the documentation. Everything is in the docs, but sometimes the examples are too dry and you must guess how to use it.)
With Racket, I miss having real threads and rich "batteries included" data structures as you'd find in Java and C#.
By real threads, I mean the ability to spawn a thread in Racket that corresponds to a new underlying operating system thread for that Racket process. Just like you get when you spawn a thread in Java.
[Balancing] Binary Search Trees as a standard that is available in many languages; B-Trees if memory locality is important; Skip Lists if there is data structure sharing between real threads and something like a Concurrent Balancing Binary Search Tree would have too much lock contention.
I have looked over the `data-red-black` library; I wish its interface was richer with `map`, `filter`, retrieving a range of values, partitioning/sliding over key-value pairs, and all the other generic sequence operations that hopefully Rhombus can enable for different underlying data structures.
These functional red/black-trees have `map` and `filter`:
I would be surprised if someone hasn't made a B-tree implementation already.
As bonus: Another red/black-tree implementation (imperative):