Haskell feels a lot like that. That code snippet gave me the same feeling of having to climb a conceptual mountain to appreciate the metaphorical view at the top. I have no doubt that there is something beautiful there, but of all of the conceptual, programming related mountains I need to climb, the language abstraction one is pretty low priority, because the language I am using really is not my limiting factor. I appreciate that others who care spend a lot of time thinking about this problem, but Haskell's got a long way to go before I'm convinced that it's worth using in everyday contexts.
I also find that code quite abstract, it looks to be solving some kind of problem that usually the language designer (if say you are in C#) would be solving. Haskell seems to be something that leaks it's mechanics into your code, where as C# keeps it for the most part nicely separated from your code, albeit with the effect of giving the programmer less power. No typeclasses etc.
"Gee, that’s complicated! I must be really smart to have
written such a function, right?
Wrong! I just have a trick!"
Seems like the author deliberately picked an example that looks hard but isn't once you understand what he wants to say.Nothing new under the sun. How about APL?
https://medium.com/@gordonguthrie/the-beam-needs-an-apl-y-la...
A professor of mine used to write commercial software in APL. (Including business apps!)
I take an old familiar tool and I instantly remember how to use it.
What gives?
When I was a boy, wise practitioners told me that being a programmer is a path of constant learning. They were right. And since then, I take learning as a natural part of the profession. I suspect it also has something to do with a high salary it commands.
Somewhere down closer to #15 is "learn Haskell," which comes after learning Rust/Go. Learning a "real" functional language would be much higher, but I already did that early in my career.
I only meant to notice that there's no impenetrable barrier to mastery of that, when desired. I don't think that to learn Haskell at a high level is harder than to do the same with C++. The latter just had more adoption for last 25 years, for a number of reasons.
In any case, some (cursory) knowledge of Haskell improved my Java, Python, and JavaScript quite a bit, and is directly applicable to Scala in many aspects.
- Harold Abelson
Looking at that code, it looks like it's working on a tree of some sort, but what kind? Something to do with Sem and State and Yo, I guess. At a wild guess, it's something to do with ASTs and DSLs or similar. But code that is dramatically inscrutable without metric shit tonnes of context, is bad code.
Really? I must be misunderstanding what you mean by "operation", or maybe this is the part of functional programming that hasn't clicked for me yet... my understanding is that the type signature tells you what kinds of things the function works with, but it doesn't tell you how the function maps the input to the output. When I hear "operation", I think of the latter. What are you referring to?
For example, if a function's type is `a -> b -> (a, b)`, there's really only one function fitting that shape: cons. What else could a (pure) function possibly do, with that typing, other than "exactly what cons does"?
There is, however, a gotcha along these lines: Haskell has a value called ‘undefined’ (along with some other issues collectively called “bottom” for CS theory reasons[1]) which can take any type. So ‘foo x y = (x, undefined)’ is a legal implementation of that function will will compile, but crash if you try to do anything with the ‘undefined’ result.
In practice, knowing your function has essentially one implementation because it’s sufficiently polymorphic (no concrete types), is still a great trick for getting the compiler to enforce certain kinds of correctness.
Do functional programmers consider the type of a variable to be more important (in some sense) than its value? If so, that could explain my confusion here, and that would be a pretty important thing to understand.
Edit: This idea -- "the type of the function completely determines the operation" -- seemed counter-intuitive to me at first, but I think that's actually a sign that it's a key concept I need to understand to really grok functional programming in a deeper way. (I think this is the case for counter-intuitiveness more generally... it's a sign that you've found the missing link in your understanding of something.) Thanks for the replies, this has been helpful.
A (somewhat faulty analogy) is imagine a method that takes in two variable odds type Object and returns a tuple of Objects. You can call any instance method on the object class, and any static method, but can't reference any class variables. And you can do any casts to or from Object. There is almost nothing left you can do but return a tuple of them. The types constrained your implementation.
Haskellers read some texts and got used to the idea of type manipulations. They know, for example, there is a type "Void" which doesn't contain any values - that is, by definition no object has this type - and therefore any function which has this type of argument can't be invoked. Because to invoke the function, it has to be passed a value of the given type, and there aren't any.
Haskellers got used to the idea that if we have function from type a to the same type a, and there are absolutely no restriction on what the type a is, then it should be identity function - they just sat on this idea, pondered it for some time and got to this conclusion. For non-Haskellers it's not clear - and even the idea that types can restrict code so tightly as to make code unique (up to isomorphisms) can be novel to them.
Does it make a bigger cognitive load? Probably not for those who got used to type properties like this. Is it hard for non-Haskellers to see this feature and try to use it? Well, many Haskellers passed through this stage, so it ought to be doable. Most importantly, can they agree on this?..
If I have ((a,b)->b), it behaves precisely the same way regardless of what those variables represent.
Do I need to do null check? Irrelevant. B is being reused, do I do a deep or a shallow copy? Does it have a copy constructor? Irrelevant.
Frame it as a game: give me a function definition and I will give you a pair of types for which that function is statically ill-defined. You must give me a function that works, without changing how it’s defined, for whatever pair of types I tell you.
Counter-intuitively, it’s actually because of ambiguity that you don’t have as much choice in how your function is implemented. If you know nothing about your types a and b, there is nothing you can do with values of those types to create new values (excluding oddities with “undefined” and the like, which I’ll ignore for the rest of this comment). The only things you can do are “structural” operations that use only some or all of the inputs you’ve been given, verbatim.
For example, given one value each of types a and b, you could have a function with a return type of a that just chose that input value and gave it back, but what other function of type a -> b -> a could you possibly have?
Now, extend that idea. Suppose you have a function of type a -> b -> (a, a). This needs to return a pair of a values, but we still only have one such value that we know. We don’t know how to make another a from an a, because we don’t know anything specific about that type. Similarly, we don’t know how to make an a from a b, or from an a and a b for that matter. So the only possible implementation of this function (with the caveat above) would be the one that returns a pair where each element is the a it was given as input.
Let’s extend that idea again. This time, suppose we have input values of types a and b, but we also have an input function of type b -> c. That is, we start with two values of ambiguous types, but we do know how to turn one of those values into a third type. In this case, we could write a function of type a -> b -> (b -> c) -> (a, c), for example, because although we don’t have an input value of type c directly, we do have a b and we know how to make a c from it. But, we have exactly one way we know to do that, so there is still only one possible implementation of a function of this type: we must take the a we started with, and we must take the b we started with and convert it to a c using the b -> c function we started with, and then we return a pair with the two final values.
When can we have more than one possible implementation? Essentially, when we have been given more than one way to make at least one of the required output values. For example, suppose we have a function of type a -> b -> (a -> c) -> (b -> c) -> (c, c). This time, we start with an a and a b, but we also know how to convert either of them to a c. We need two c values in our output, but nothing here says they have to be the same, so each of the output values could come from either of the a and b inputs, giving four possible implementations (but only four).
As a final example, as surprising at it might seem at first sight, there is no possible implementation at all of a function of type a -> b (again, excluding funny games with “undefined” and the like). We simply don’t have any way to make a b without knowing anything about the type itself and without being supplied with either a b value directly or some way to make one.
If you found that interesting and really want to blow you mind, you might enjoy the Curry-Howard isomorphism. The Wikipedia page isn’t a good introduction if you’re trying to understand it, IMHO, but Wikibooks has a nice introduction:
https://en.wikibooks.org/wiki/Haskell/The_Curry%E2%80%93Howa...
Leveraging this rules out bugs.
hoistStateIntoStateT
:: Sem (State s ': r) a
-> S.StateT s (Sem r) a
?Sorry even that'll get my brain CPU on 100% for a few minutes.
I have never user Polysemy, any effects library, or type-level lists before, but I'm reasonably sure my guess is right, despite not understanding any of the code.
The reason is simple: as a developer looking at the code written a week ago by myself, using "normal" language, I'm able to somewhat recognize what's going on, after a month or two it's something produced by aliens. With code depicted above this time will shorten to what? hours? days? Additionally it will require additional effort for developer himself or anyone else to decrypt it after a while.
...
...I definitely need to use my brain to read that code though.