I think I will read the pragmatic programmer by david thomas and be done with learning "general" software engineering from books. The book mentions learning a programming language every year, but I personally wouldn't learn a new programming langauge every year just to learn new things. If you are going to specialize in emacs, then it makes sense to learn elisp. If I'm going to learn a language, I better learn a language that I'm going to use very often. I don't advocate learning a language you are not going to use. Learning a language you know that you are not going to use regularly is a waste of time.
The reason they recommend that is that different languages teach you different things, different approaches to solving problems, different ways to think about problems, etc. There's not much point in learning similar languages, but there's a lot of value in learning very different languages. I think "every year" is a very aggressive ideal. I think "every couple of years" is reasonable. It's why I've learned Standard ML, Ruby, Python, Elm, Kotlin, and a few others, over the same period of time I've been using Clojure in production (and some Scala early on). It's also why I recommend the "Seven Languages in Seven Weeks" book: it's a great way to get a tour of some very different ideas about solving problems.
I learned clojure and don't use it for work, but my thinking and design is better for it. It taught me a lot about programming and basically rewired my brain because of how different it is from the traditional oop that I learned in school and getting all the popular books
Not learning it because I don't use it regularly would have been doing myself a huge disservice
I regret having learned haskell because its type system couples nominal types with functions and promotes place-oriented programming at the type level.
data Type a b c d e f = Constructor1 (Int a) | Constructor2 (String b) | ...
This is place-oriented programming at the type level. Haskell type system is also needlessly complex.
In general, I want to focus on simple languages that avoid place-oriented programming and avoid coupling between (nominal) types and functions. I want to be efficient with my time by learning from the best. Static typing with robust type inference, structural types, polymorphism, and other features may be able to decouple (nominal) types from functions.
Also, if you are already busy learning languages that you are going to use regularly, then don't sacrifice recovery and other aspects of life for education by learning a separate educational language. I also want to be efficient with time. Human lifespan is short.
Personally, because I will be busy learning languages that I will use regularly, I don't want to sacrifice my life by adding an educational programming language on top of everything.I don't recommend learning educational programming languages at the expense of other aspects of your life. If you have spare time, then it's okay to learn an educational programming language. Personally, I have seen enough programming paradigms that learning a new programming language that I'm not going to use doesn't feel like a good use of my time. If I'm going to learn something just for education, it better have a substantial return on investment, considering my short human lifespan.
If you've been exposed to a variety of programming paradigms, I think you've already followed the advice. The point is breadth, and to encourage developers not to call it "done" because they've learned a single programming language. Even within the same paradigm, different languages have different philosophies and tradeoffs. When you expand to other paradigms, you've added new thinking tools to your mental toolbox. You've done this and evaluated what you've learned. Sounds like a "pragmatic" approach to me
I know fp style prefers plain return values, so when designing API's, what's a good way to deal with errors if not exceptions?
I'm not sure that's true. Clojurists probably prefer simpler types, but a lot of FPers would probably advocate for something like a Result type (Result monad, even).
At that point though, shouldn't I just use the exceptions system?
I'm not sure I have a good answer. I'm curious to see what others think.
Exceptions are fine for "exceptional" failures, i.e., unexpected situations. But not for flow of control stuff.
And you can't avoid them when doing interop.
For errors in general it depends what failure information you need to return
Having built integrations for a handful of third party systems, I'd say unexpected situations are to be expected! 😉
A generic failure (or success), maybe returning nil is sufficient. Clojure's nil-punning really helps here.
An HTTP 500 failure, would we consider that "exceptional?" I lean toward yes
If you need more details from a failure, or where the success value can be nil, return a map with either :error or :value can be a good compromise
Sure, 500 might be unexpected. But for some 50x you might want to retry with backoff
So, "it depends" in most cases
We don't generally use monads in Clojure because monads really need a static type system to make sense. There's a Contrib library algo.monads but it is not used much. Monadic code without type-based dispatch is a bit painful to write -- and monadic patterns tend to spread into surrounding code which can be ugly in Clojure.
The map with :error / :value is about as close as we get there but it fits well with Clojure's nil-punning:
(let [{:keys [error value]} (call-the-fn arg1 :arg2 "arg3")]
(if error
... handle the error ...
... handle the value ...))
Similarly, if you have a simple nil (error) / non-`nil` (value) return:
(if-let [result (call-the-fn arg1 :arg2 "arg3")]
... handle the result ...
... handle generic failure ...)And those combine well with exceptions since you can wrap the call with (try ... (catch Exception e ... report it ...)) and either return nil from the catch or some :error (ex-message e) map value etc.
For an API, however you want to model it, I like to group things into: 1. Retryable errors The call is valid but some issue you know can succeed on retry occurred. Clients should retry and only alert yourself if multiple retries fail. 2. NonRetryable errors The call is valid, but some issue you know can't succeed on retry occurred. Immediately alert yourself and clients should not retry as that's futile. 3. Client caused errors The call was invalid, or permissions are missing, or some client managed configuration is incorrect, etc. The client isn't expected to retry, but it's on them to fix. You don't alert yourself on those, but you can check on them weekly.
monadic failure types don't really need static typing, but they do need pattern matching to be ergonomic. (they're used very effectively in Erlang without static types)
since we don't have pattern matching in the core language, you can't expect that the users of your library will have it, so you can't really design your API around failure types without making it a tremendous pain in the ass to use
"railway-oriented programming" (or is it "railroad-oriented..."?)
See https://fsharpforfunandprofit.com/posts/recipe-part2/ but also https://fsharpforfunandprofit.com/posts/against-railway-oriented-programming/ (linked from the first as a follow-up)
I have also seen this: https://github.com/cognitect-labs/anomalies --- perhaps interesting to consider; I don't think I'd advocate for it in many situations, but it's another line of thinking in this area, for sure.
You can do what ring async does and cps transform your code with callbacks, the continuation and the error continuation Then you don't raise an exception, just call the error continuation withsome value to return to the top level caller
it's "it depends" all the way down https://www.daveliepmann.com/articles/idiomatic-clojure-errors.html