> And I've had a lot of fun micro-optimizing Haskell code for Project Euler problems when I was studying.
Sounds a lot like my experience. I never really used Haskell for "real work", where I need support for high-performance numerical calculations that is simply better in other languages (Python, Julia, C/C++, Fortran).
But learning functional programming through Haskell – mostly by following the "Learn you a Haskell" book and then spending time working through Project Euler exercises using it – had a quite formative effect on how I write code.
I even ended up baking some functional programming concepts into my Fortran code later. For instance, I implemented the ability to "map" functions on my data structures, and made heavy use of "pure functions" which are supported by the modern Fortran standard (the compiler then checks for side effects).
It's however hard to go all the way on functional programming in HPC contexts, although I wish there were better libraries available to enable this.
I think it is a shame Haskell has gained a reputation of being hard, because it can be an enriching learning experience. Lots of its complexity is accidental, and comes from the myriad of language extensions that have been created for research purposes.
There was an initiative to define a simpler subset of the language, which IMHO would have been great, but it didn't take off: https://www.simplehaskell.org. Ultimately, one can stick to Haskell 98 or Haskell 2010 plus some newer cherry-picked extensions.
Interestingly, Elm has inspired a host of "successors", including Gleam + Lustre, which look really great (I haven't had a chance to really try them yet).
I've tried and failed several times to write Haskell in an Elm style, even though the syntax is so similar. It's probably me (it's definitely me!), but I've found that as soon as you depend on a library or two outside of prelude their complexities bleed into your project and eventually force you into peppering that readable, simple code with lifts, lenses, transformations and hidden magic.
Not to mention the error messages and compile times make developing in Haskell a chore in comparison.
p.s. Elm has not been abandoned, it's very active and getting better every day. You just can't measure by updates to the (stable, but with a few old bugs) core. For a small, unpopular language there is so much work going into high quality libraries and development tools. Check out
https://elmcraft.org/lore/elm-core-development
for a discussion.
Elm is so nice to work in. Great error messages, and near instant compile times, and a great ecosystem of static analysis, scaffolding, scripting, and hot reloading tools make the live development cycle super nice - it actually feels like what the lispers always promised would happen if we embraced repl-driven development.
I feel like I understand that perspective, but I also don't think I'm wrong in claiming Elm has been effectively abandoned in a world where an FAQ like that needs to be written.
I'm not going to try to convince you though, enjoy Elm!!
With haskell you can look to the ecosystem to see how to accomplish specific things with a pure functional approach. When you look at ocaml projects in that way you often find people choosing not to.
In my time learning Haskell a decade ago, it was rare to find some code that wasn't using an experimental extension.
Except the obvious functional programming concepts, one thing I enjoyed exploring in Haskell was “infinite data structures” and “lazy evaluation” – like how you could provide a recursion relation to define an infinite list of numbers, but only the finite elements in the list you ask to print are actually calculated behind the scenes. This “lazy evaluation” can be used to build algorithms that would be completely unreasonable in any other language.
I also liked how easy it is to define new data structures. Like implementing a minimal tree data structure can be done in literally one line of code, which is just ridiculous compared to most languages that I’ve tried.
In real life, I find that Haskell suffers from trying too hard to use the most general concept that‘s applicable (no pun intended). Haskell programs happily use “Either Err Val” and “Left x” where other languages would use the more expressive but less general “Result Err Val” and “Error x”. Also, I don’t want to mentally parse nested liftM2s or learn the 5th effect system ;-)
Whoever wrote that is wrong.
You are the one who is wrong, I'm afraid.
Rust's most important feature! The bootstrapped implementation.
Additionally, unlike some languages that are formally specified before turning to implementation, Rust has subscribed to design-by-implementation. The implementation is the language.
https://github.com/mozilla/rust/tree/ef75860a0a72f79f97216f8...
I'm not one to hold that one shouldn't form tentative conclusions until one "has all the fact". Also, I'm not one to hold that readers should trust the opinion of an internet comment writer they know nothing about. I could write a long explanation to support my opinion, but I'm probably not going to.
If not, what is the relevance of your comment?
> Haskell (GHC): typeclasses, type families
Although SML is older (1983), OCaml is younger than Haskell.
In Python, it's very easy to just write out a for-loops to do these things, and you don't necessarily go looking for alternative ways to do these things unless you know the functional equivalents already. But in Haskell you're forced to do things this way since there is no for-loop available. But after learning that way of thinking, the result is then more compact code with arguably less risk of bugs.
map(function, iterable)
That seems very logical to me, but then, I’m not a functional programmer, I just like map. It’s elegant, compact, and isn’t hard to understand. Not that list comps are hard to understand either, but they can sometimes get overly verbose.filter has also lost ground in favor of list comps, partially because Guido hates FP [0], and probably due to that, there has been a lot of effort towards optimizing list comps over the years, and they’re now generally faster than filter (or map, sometimes).
[0]: https://www.artima.com/weblogs/viewpost.jsp?thread=98196
map(func4, map(func3, map(func2, map(func1, iter))))
vs iter.map(f1).map(f2).map(f3).map(f4)
I made up the syntax for the last one, but most functional languages have a nice syntax for it. Here's F#: iter |> f1 |> f2 |> f3 |> f4
Or plain shell: command | f1 | f2 | f3 | f4Use generator syntax, which is really the more pythonic way to it.
>>> iter = [1,2,3,4]
>>> f1 = lambda x: x*2
>>> f2 = lambda x: x+4
>>> f3 = lambda x: x*1.25
>>> [f3(f2(f1(x))) for x in iter]
[7.5, 10.0, 12.5, 15.0] items = [1,2,3,4]
gen1 = (x*2 for x in items)
gen2 = (x+4 for x in gen1)
gen3 = (x*1.25 for x in gen2)
result = list(gen3)
It's nicer in a way, certainly closer to the pipe syntax the commenter your replying to is looking for, but kind of janky to have to name all the intermediate steps.Second, that's all good and well if all you want to do is map. But what if you need combinations of map and filter as well? You're suddenly dealing with nested comprehensions, which few people like.
In F#, it'll still be:
iter |> f1 |> f2 |> f3 |> f4
Here's an example from real code I wrote: graph
|> Map.filter isSubSetFunc
|> Map.filter doesNotContainFunc
|> Map.values
|> Set.ofSeq
This would not be fun to write in List Comprehensions, but you could manage (only two list comprehensions). Now here's other code: graph
|> removeTerminalExerciseNodes
|> Map.filter isEmpty
|> Map.keys
|> Seq.map LookUpFunc
|> Seq.map RemoveTrivialNodes
|> Seq.sortBy GetLength
|> Seq.rev
|> Seq.toList
BTW, some of the named functions above are defined with their own chain of maps and filters. iter = [1,2,3,4]
f1 = lambda x: x*2
f2 = lambda x: x+4
f3 = lambda x: x*1.25
result = iter
for f in [f1, f2, f3]:
result = [f(v) for v in result]
Then the list comprehension can be moved up to mimic more closely what you're doing with F#, allowing for operations other than "map": result = iter
for f in [
lambda a: [f1(v) for v in a],
lambda a: [f2(v) for v in a],
lambda a: [f3(v) for v in a],
]:
result = f(result)
And a step further if you don't like the "result" reassignment: from functools import reduce
result = reduce(lambda step, f: f(step), [
lambda a: [f1(v) for v in a],
lambda a: [f2(v) for v in a],
lambda a: [f3(v) for v in a],
], iter)In my F# file of 300 lines[1], I do this chaining over 20 times in various functions. Would you really want to write the Python code your way every time, or wouldn't you prefer a simpler syntax? People generally don't do it your way often because it has a higher mental burden than it does with the simple syntax in F# and other languages. I don't do it 20 times because of an obsession, but because it's natural.
[1] Line count is seriously inflated due to my habit of chaining across multiple lines as in my example above.
For example: https://ryi.medium.com/flexible-piping-in-python-with-pipey-...
And another mentioned there: https://pypi.org/project/pipe/
> It's certainly not as clean as F# but neither is it as bad as the original example if there's a lot of functions
This is still backwards?
filter(lambda x: x<5, map(lambda x: 2*x, [1,2,3,4,5]))
filter (<5) . map (*2) $ [1,2,3,4,5]
(Technically the Python version should be cast to a list to have identical behavior.)Same with comprehensions (although nesting comprehensions will always get weird).
[x for x in [2*x for x in [1,2,3,4,5]] if x<5]
[x | x <- [2*x | x <- [1,2,3,4,5]], x<5]Laziness is mostly an anti-pattern.
My take on this is that this was all nice and interesting but a lot of this stuff was a bit academic. F# is probably the closest the community got to having a mature tooling and developer ecosystem.
I don't use Haskell myself and have no strong opinions on the topic. But usually a good community response to challenges like this is somebody stepping up and doing something about it. That starts with caring enough. If nobody cares, nothing happens.
Smalltalk kicked off a small tool revolution in the nineties with its refactoring browser. Smalltalk was famous for having its own IDE. That was no accident. Alan Kay, who was at Xerox PARC famously said that the best way to predict the future was to invent it. And of course he was (and is) very active in the Smalltalk community and its early development. Smalltalk was a language community that was from day one focused on having great tools. Lots of good stuff came out of that community at IBM (Visual Age, Eclipse) and later Jetbrains and other IDE makers.
Rust is a good recent example of a community that's very passionate about having good tools as well. Down to the firmware and operating system and everything up. In terms of IDE support they could do better perhaps. But I think there are ongoing efforts on making the compiler more suitable for IDE features (which overlap with compiler features). And of course Cargo has a good reputation. That's a community that cares.
I use Kotlin myself. Made by Jetbrains and heavily used in their IDEs and toolchains. It shows. This is a language made by tool specialists. Which is why I love it. Not necessarily for functional purists. Even though som Scala users have reluctantly switched to it. And the rest is flirting with things like Haskel and Elixir.
The languages of engineering-aligned communities may appear to have won the race, though they have been adopting significant ideas from Haskell and related languages in their victories.
Haskell's biggest benefit is functions, not methods. To define a function, you need to stop directly mutating, and instead rely maps, folds, filters, etc. The bargain was: you give up your familiar and beloved for-loops, and in return you get software that will yield the same output given the same input.
So what happened with the adoption? The Java people willingly gave up the for-loops in favour of the Streams/maps/filters. But they didn't take up the reward of software that yields the same input given the same output.
What's something else in the top-5 killer Haskell features? No nulls. The value proposition is: if you have a value, then you have a value, no wondering about whether it's "uninitialised". The penalty you pay for this is more verbosity when representing missing values (i.e. Maybe).
Again, the penalty (verbose missing values ie. Optional<>) was adopted, and the reward (confidently-present values) was not.
JavaScript promises can work analogously to the Maybe monad if you want them to.
Swift’s optionals are essentially the same thing as the Maybe monad.
Ah, the joys of having a Scala `Option` type and still having to consider the cases or Some[thing], Nothing and... null!
Yes, well-written Scala code knows not to use null with reckless abandon, but since when your coworkers coming from Java know to show restraint?
I'd say it's more of a reflection of how having a very big company funding the language is making a difference.
People like to link Haskell's situation to its academic origins, but in reality, most of the issues with the ecosystem are related to acute underfunding compared to mainstream languages.
Either the community is large enough for it, or it comes from the sponsoring company.
Few languages start off by being in the first situation. The first example that comes to my mind (Python), well... Tooling was a long and painful road. And if the language hadn't been used/backed by many prominent companies, I don't see how man-hours would have flowed into tooling.
Also time wise it's a good example because python emerged early nineties around the same time the Haskell community started forming. Haskell had a few years head start actually.
The difference was that python became popular quite early in things like Linux distributions and even though Haskell was available and similarly easy to install in those, it never really caught on. Sponsored development usually happens as a result of people finding uses for a language, not before.
I hear this a lot, but am curious about two things: (a) which bit(s) of the toolchain are you thinking about specifically -- I know HLS can be quite janky but I haven't really been blocked by any tooling problems myself; (b) have you done much Haskell in production recently -- i.e. is this scar tissue from some ago or have you tried the toolchain recently and still found it to be lacking?
I’ve experienced this too, the tools can certainly be improved, but also a little more understanding of what they do and how to interpret their error messages could help you (I am guessing).
Java has recently added sealed classes/interfaces which offer the same features as sum types, and I would argue that Java is definitely mainstream.
Kotlin has a similar feature. It might be used less than Java, but it's the default language for Android.
Swift has `enum` for sum types and is the default language for iOS and MacOS.
Likewise for Rust, which is gaining traction recently.
Typescript also has union/sum types and is gaining lot of traction.
How does the ABI work? If a library adds a new constructor, but I am still linking against the old version, I imagine that it could be reading the wrong fields, since the constructor it's reading is now at a different index?
optional_int: int | None = None
Python's `A | B` is a union operation, but in Rust a sum type is always a disjoint union. In Python, if `A = B = None`, then `A | B` has one possible instance.
In Rust, this sum type has two possible instances. This might not sound like a big deal, but the semantics are quite different.
def foo(int | None = None) ...
... just means the variable's default value is None in a function definition. But it could be either in an actual function call.
Say you wanted to define some function taking `YYMMDD | MMDDYY`. If both YYMMDD and MMDDYY are just aliases to `str`, then you gain no information, you cannot discriminate on which one it is, since the union `str | str` just reduces to `str`.
Sum types are disjointed unions, you can't just say `str | str`, the terms are wrapped in unique nominal data constructors, like:
enum Date { MMDDYY(String), YYMMDD(String) }
Then when accepting a `Date` you can discriminate which format it's in. You could do the same in Python by defining two unique types and using `MMDDYY | YYMMDD`.
Exactly the same for me.
> Granted, the tooling is sh*t.
Stack and Stackage (one of the package managers and library distribution systems in Haskell-land) is the best I found in any language.
Other than that I also found some tools to be lacking.
All other ecosystems there are some libs that do not work together well, and usually you find out the hard way.
This is valuable because you as the developer have a lot less stuff to think about when you're trying to reason about your program's behavior.
>> Would you mind explaining what you mean by stateless?
> the same input to a function produces the same output every time
It's good that the question about 'stateless' was raised, but because these are two different things. Working with pure functions does indeed have the above benefits, but a dumb web node deferring its behaviour to a stateful database is not stateless in that sense, and so does not have the above benefits.
FP statelessness means no mutable state, so two identical GETs will of course return the same result.
And I will as strongly as possible emphasize the opposite you should not.
If you are are already experienced in functional programming, as well as in statically typed functional programming or something lovely in the ML family of languages then only then does Haskell make sense to learn.
If you are looking to learn about either FP in general, or staticly typed FP Haskell is about the single worst language anyone can start with. More people have been discouraged from using FP because they started with Haskell than is probably appreciated. The effort to insight ratio for Haskell is incredibly high.
You can learn the majority of the concepts faster in another language with likely 1/10th the effort. For general FP learn clojure, Racket, or another scheme. For statically typed FP learn F# or Scala or OCAML or even Elm.
In fact if you really want to learn Haskell is is faster to learn Elm and then Haskell than it is to just learn Haskel. Because the amout or weeds you have to navigate through to get to the concepts in Haskell are so high that you can first learn the concepts and approach in a tiny language like Elm and it will more than save the amount of time it would take to understand those approaches from trying to learn Haskell. It seems unbelievable but ai found it to be very try. You can learn two languages faster than just one because of how muddy Haskell is.
Now that said FP is valuable and in my opinion a cleaner design and why in general our industry keeps shifting that way. Monoids, Functors, Applicative are nice design patterns. Pushing side effects to the edge of your code (which is enforced by types) is a great practice. Monads are way overhyped, thinking in types is way undervalued. But you can get all of these concepts without learning Haskell.
So that's the end of my rant as I've grown tired of watching people dismiss FP because they confuse the great concepts of FP with the horrible warts that come with Haskell.
Haskell is a great language, and I'm glad I learned it (and am in no way an expert at it)- but it is the single worst language for an introduction to FP concepts. If you're already deep in FP it's and awesome addition to your toolbox of concepts and for that specific purpose I highly recommend it.
And finally, LYAH is a terrible resource.
Could you elaborate? I know LYAH doesn't teach enough to write real programs, and does not introduce necessary concepts such as monad transformers, but why is it so terrible as an introduction to Haskell and FP? (In my mind, incomplete/flawed != terrible... Terrible means "avoid at all costs").
As for your overall point, I remember articles posted here on HN about someone teaching Haskell to children (no prior exposure to any other prog lang) with great success.
Bunch of places where the tone masked or downplayed real issues in ways that made other text suspect. As a concrete example, `head [] -> Exception` with something like "of course it errors if you take the first part of something that's not there" and `take 1 [] -> []` with "obviously taking one thing from an empty list gets you an empty list" -- uh, no. Maybe it's a historical wart, maybe there are good technical reasons, but different behavior in these cases is definitely not obvious!
That said the language is small enough you can go through the tutorial in a weekend. You'll know pretty quickly if you feel like you're picking it up or feels too foreign.
My gut feel is general programming experience is enough, but don't hold me to that one.
The type system in Haskell, particularly union types, is incredibly powerful, easy to understand for the most part (you don't need to understand monads that deeply to use them), and highly useful.
And I've had a lot of fun micro-optimizing Haskell code for Project Euler problems when I was studying.
Give it a try. Especially, if you don't know what to expect, I can guarantee that you'll be surprised!
Granted, the tooling is sh*t.