The Cult of the Haskell Programmer
wired.com
wired.com
Haskell might have something to do with spreading FP more widely, but it's hardly the origin of the movement. No mention of ML. No mention of lisp, APL, Miranda, ...
Could you elaborate more about what you mean, maybe giving some examples?
[1] https://www.haskellforall.com/2016/04/worst-practices-should...
Literally was in a meeting where a new hire was given the green light because "she was a language lawyer but not enough of a language lawyer to get in the way". Fuck that.
https://pages.cpsc.ucalgary.ca/~robin/class/449/Evolution.ht...
Where a simple program goes through levels of increasing complexity before arriving at:
fac n = product [1..n]People always assume garbage collectors in Haskell might somehow be different due to immutability. But due to laziness it works just like Java garbage collection because laziness requires mutability.
When you have something like `doThis >> doThat` (two actions sequentially) the first action results in a token (RealWorld#) that must be passed into the second action. Even though the evaluation of the two actions may be undetermined in a lazy language (but determined in a strict language), the token being passed around has a definite order.
https://www.microsoft.com/en-us/research/publication/wearing...
a = readLine()
b = readLine()
print(b)
print(a)
What order are the lines executed in?In a strict language, the answer is obvious. In a non-strict language, line 4 has a data dependency on line 1 so always executes after it, ditto lines 3 and 2. But how the two groups get interleaved is completely unpredictable, so, if you’re really committed to non-strict evaluation, you need a way to force data dependencies between all four lines such that order of evaluation is forced. Once you achieve that, you have a bunch of ugly-looking machinery that’s peppered across your whole codebase.
Monadic IO (and do-notation in particular) gives us a way to write this sort of code without feeling the urge to gouge our eyes out.
the order of operations, specifically printing a prompt and reading the answer. Before someone (Mogi?) realized that a category theoretical monad with specific rules made IO sequencing trivial, the state of the art (IIRC) was world-passing. (With world-passing, you have a value that represents the state of everything outside the program; any IO operation absorbs the current world value and returns a new one. Only the most current value actually works.)
I don't know if it is still the case, but the last time I poked around, in GHC the IO monad was implemented using the older world-passing code.
Raku balances imperative, functional and OO styles in a unified syntax. Functional programming primitives include currying, lazy and eager list evaluation, junctions, autothreading and hyperoperators (vector operators).
since the name change to raku some years ago, the tussling over the future of perl has declined to nothing
on the other issues, well many OSS communities suffer from falling out of individuals at times - all the same the raku commit unity is healthy and large enough to be making slow but steady progress toward the release of v6.e, with a lot of cool features already in preview
it’s a supportive and active community and i’d invite you to come over to Discord or IRC and say hi
here are the stats on git rakudo since 20 April…
_Excluding merges, 7 authors have pushed 133 commits to main and 181 commits to all branches_
https://github.com/lsmor/snake-fury
You build a snake game in stages.
But the whole no side effects thing... I just can't wrap my head around monads. I understand the benefits of no side effects, I still can't figure out how to write code, test it, improve, you know, iterate over it. It feels to me like you need to have your entire program understood and specified before you can write a single line of code.
That, and it seems like you need to have a complete understanding of the entire standard library just to get anything done. When I go into Haskell community groups to get some help, I'm usually met with walls of things I don't understand, which is why I'm there I'm the first place.
If I could get over these two road blocks I really think I'd write everything I could in the language. I hope to do that some day soon, but it's really hard when you're expected to already get it by the community. I just want to know how to do IO without a complete refactor and be able to read some documents on a library and start using it.
I used no libraries and wrote all the math myself, because when I went to investigate libraries none of them seemed to explain what they do in language someone who didn't already know would understand. When I went in stackexchange and other community spaces I got similar results.
import qualified Data.Text as T
import qualified Data.Text.IO as T
...
-- maybe you're reading things into a Set or something named acc
go acc = do
eof <- isEOF
if eof then pure acc
else do
line <- T.getLine
T.putStrLn ("LINE IS "<>line)
go (insert line acc)
There's hGetLine, hPutStrLn and hIsEOF for file handles, use withFile https://hackage.haskell.org/package/base-4.19.1.0/docs/Syste... which will close the handle when you exit the block. There are many more ways of doing this stuff of course, but the above will work fine for most stuff without any special libraries.In general, use Data.Text for anything that you don't consider binary data (human-readable text as well as code, config files and such). Use Data.ByteString if you're thinking of it as pure encoded bytes.
Could you post some examples of interactions you had on stackexchange? I'm interesting in improving the onboarding experience for new users, and knowing what do avoid will be helpful.
https://web.archive.org/web/20171020034308/https://crsr.net/...
Or at least that was my experience.
main = do
putStrLn "hello world"
more
more = putStrLn "hello again, still in IO"
If you want to, you can break the rule and do I/O from non-IO functions, by using trace: import Debug.Trace
add :: Int -> Int -> Int
add a b =
trace ("adding " ++ show a ++ " and " ++ show b)
(a + b)
or unsafePerformIO: import System.IO.Unsafe
add :: Int -> Int -> Int
add a b = unsafePerformIO $ do
appendFile "debug.log" ("adding " ++ show a ++ " and " ++ show b ++ "\n")
return (a + b)
Not exactly white-hot hoops of fire ? :)Ironically, I think the syntax probably is causing some of your problems here. Haskell seems to have a problem where the syntax is so dense that simple operations get confusing. And the strong typing pulls a lot of the learning pain up front. You might be better off learning functional programming independently of Haskell's type system to get a feel for how to do it, then come back and get the types correct and learn the advanced tricks.
If you want an unsolicited tip; don't bother trying to "understand monads", there isn't much there to understand - they are what they are (it doesn't get much more basic than the Maybe monad). Look at the code that uses monads - they are an answer to being unable to solve problems the imperative way. So the point of a monad is programmers want to write code in a certain way and without monads there would be a monad-shaped hole in the code that needs to be filled. If the code was written in a dynamic imperative style, monads wouldn't be so interesting.
A chunk of monads are just obviously necessary and simplifying once you start using ... whatever the Haskell equivalent of a pipe or threading macro is ... and make the mistake of trying to use functions designed for an imperative style.
That's the secret to Haskell. There's no "getting it". There's just using it.
Motivating the use of monads in FP requires solving a bunch of problems without, and realise your life would be so much easier if you abstracted out the commonality at the function composition level. But there isn’t time for that.
When you skip the motivation, the explanation becomes “Welcome to FP. We’re gonna jump through these hoops because math is beautiful, also, most of the syntax you’re typing gets thrown away at compilation, good luck and fmap fmap fmap!”
You are right. But to someone who doesn't already know what a monad is, these words don't mean much, because they are very abstract.
I personally started to understand monads when I learned why monads are useful. They are a tool. And if someone just hands you a tool without any explanation what to use the tool for, you will most likely not be able to do something with that tool.
Monads are useful, because they make it possible to compose a sequence of computations in such a way, that failure/abortion anywhere in the sequence of computations is handled gracefully. This is relevant, because computer programs can fail all the time because of bad data or bad network connections. Monads allow for elegant error handling and fall-through.
The talk is about segmenting IO and mutation from the rest of your program. The core of your program should be just pure functions and the IO part of your program should be "at the edge".
For example, consider a game loop. Game loops tend to have a while loop like this:
function loop() { while (should_not_close_window) { update(); // Run update function which updates mutable state draw(); // Draw from the mutable state } }
If we wanted to segment IO, we would return an immutable type/value from the update function instead like:
function loop(old_state) { const new_state = update(old_state); // Call a function which returns a new state given an old_state. draw(new_state); // Draw function still uses IO because drawing is inherently imperative
if (new_state.should_not_close_window) {
loop(new_state); // Call the loop function with the new state; this tail recursion replaces the previous while-loop). The program automatically exits the loop if new_state.should_not_close_window is false.
}
}The key thing is to turn as much of your program into a function that takes a value and returns a new value as possible, and let the IO code be only there for operations which cannot be done without IO (like drawing).
There are some cases where you might find yourself wanting to perform IO inside the update call but that can be handled by setting some state (inside your root state object) which the boundary of your program interprets as "do IO at the boundary".
For example, let's add an async handler to the loop (async often being IO like disk writes).
// Function to handle async messages function handleAsync(old_state) { for (const msg of old_state.msgs) { execMsg(msg); // Call function to pattern match and execute this specific message } const new_state = remove_msgs(old_state); // Call function that returns same state, except msgs is now the empty list (because the msgs have been handled) return new_state }
Where type msgs could be a list of types like | WriteFile of string | MaximiseScreen
The main loop will be modified by putting a call to this handleAsync function after update, like this:
function loop(old_state) { const mid_state = update(old_state); const new_state = handle_async(mid_state); draw(new_state);
if (new_state.should_not_close_window) {
loop(new_state);
}
}The majority of your code should be in the pure update() function which returns a new value (this example doesn't explain anything about pure functions at all) and IO should be minimised.
Generally, don't do IO (except at the boundary/root of your application). When you want to do IO, add some kind of message/datatype value to your state object and do the IO when your state object bubbles up, rreturning to the root of your application.
The talk likely explains this better but I hope my attempt at giving an explanation was helpful.
If you'd like to share any more about your experience I would really value hearing about it. Perhaps you could link to forum discussions you've been involved in, documentation you found confusing, or programs you've written that you got stuck on.
You're also welcome to contact me privately if you like. My contact details are in my profile.
A few direct observations based upon what you said:
- working with monads takes some practice and understanding. There are several things that need to be learned to work with them, none of which really are directly addressed by educational materials. My advice here is to find someone who is willing to help you work through direct, concrete problems you're having, and/or work though a well-designed resource, such as haskellbook.com. You will still likely need another resource to talk to about issues tho.
- re: entire program understood before writing any code: I am not sure specifically what you mean here, but I think you're talking about how making a change in haskell can necessitate quite a large refactoring, where in another language it might be a trivial change. This is... indeed sad at times, but otoh, this is intractably linked with _other_ notions in haskell: functions should be total, side effects should be isolated, etc. So you _will_ need to do a refactor if you need to introduce side effects to a new part of your program. This is how you get the benefit of having isolated effects.
- re understanding the entire standard library: yeah, this is part of the unfortunate reality. Haskell is an active research language, and that research has borne fruit. So, practices have changed, which means that there is a lot of legacy gotchas around. Not only that, but things that you might do trivially in another language may require using a seemingly esoteric functionality in haskell (traversable imo is the canonical exemplar). Overall this means that learning haskell is a much larger effort than it could otherwise be.
Thus, I think there is certainly room for a haskell successor that addresses these issues, however, this doesn't exist yet, and haskell is still the best option we have at this point.
Why did nobody go for the actually useful sweet spot - allowing procedural/local mutation, but making the procedures themselves 'pure' and taking care to control side effects on the program scale?
This would allow for things like actually useful time travel debugging and runtime code modification while keeping the performance and familiarity of procedural languages.
This is what Haskell does by encouraging you to bundle the part of your code that does mutation into, for example, a state monad, and pass the result of the monadic computation to other pure functions.
However, I meant specifically that OCaml is so 'pragmatic' that they have a for-loop as a built-in syntactic element in the language. They also have mutable variables built-in. In eg Haskell those are 'only' available as part of the standard library.
for x in range(10):
print(x)
forM_ [0..10] $ \x -> putStrLn xWhy do you expect to understand the syntax of a language you never studied?
There's, you know, also the actual parentheses that everyone already knows.
> Why do you expect to understand the syntax of a language you never studied?
Because most languages have a syntax that you can grasp almost immediately, since they conform to a common syntax, rather than invent everything all over again. Then you can focus on the actual features of the language and are not bogged down by syntax.
Those are also available, and are legal syntax in Haskell.
> Because most languages have a syntax that you can grasp almost immediately, since they conform to a common syntax, rather than invent everything all over again.
I hope you don't count C's `for(something, something; something; something) {` as part of that intuitive bunch?
Btw, Haskell didn't re-invent the wheel for most of its syntax: they stuck to the common and already established syntax of the ML family of languages. ML is from 1973. (For comparison, Wikipedia says C is from 1972.)
I don't especially like the idea of making multiple notations for the same thing, to be honest.
> I hope you don't count C's `for(something, something; something; something) {` as part of that intuitive bunch?
Not my favorite, but at least widely used. I find the style in for example Rust the most easy on the eye.
> Btw, Haskell didn't re-invent the wheel for most of its syntax
Yeah I actually suspected this was the case.
I can appreciate that sentiment, but I'm afraid the designers of Haskell had a different approach there.
There often a few different ways to express something. But at least they can often be understood as thin syntactic sugaring of one into the other. It's not like C++ or Perl (or even Ruby with its different types of closures) where you have tons of overlapping ways to express something, and they are all subtly different.
> I find the style in for example Rust the most easy on the eye.
I'm not sure about easy-on-the-eye, but I find Rust mostly quite bearable, too.
It's also very difficult to achieve in practice. According to the Zen of Python "There should be one-- and preferably only one --obvious way to do it" but I don't think even Python lives up to that.
withTempFile "foo" $ \tmpFile -> do
<do something with tmpFile>
flip fmap myList $ \e -> do
<do something with each element i>
And you can even create your own constructs. You're not limited to the ones that are built in.(The [0..10] is actually syntax, and I think that's quite unfortunate. `range 0 10` would have been just as good.)
Well, Haskell is in fact flexible enough that you can define that range function.
But one reason to make it syntax is that you can define things like [0..], which would be between awkward to impossible to do with a single `range` function in Haskell.
https://www.stackage.org/haddock/lts-22.22/base-4.18.2.1/Pre...
https://www.stackage.org/haddock/lts-22.22/base-4.18.2.1/Pre...
But overall, Haskell isn't really any worse here than Rust's for-each; or even C's weird for-loop (where you just have to rote learn what the three parts in the header of the loop do). I agree it's worse than Python's or Rust's vanilla for-loop.
I mostly like Python's syntax, I just wish they would make their would declare their indentation syntax to be sugar for a curly brace syntax (like Haskell does), and their would make their statements and blocks give values, like Haskell and Rust do.
(Of course, I have some other things I don't like in Python. But her syntax is by-and-large fine.)
> Emacs/Lisp forever
I think of Haskell as the declarative statically typed lisp I’ve always wanted.
Both Rust and Haskell are very opinionated in the way the program is organized. Adding a seemingly small feature can often result in larger-than-expected refactoring. (In Haskell, one might need a new monad transformer layer or convert non-monadic code to monadic; in Rust one might have to rethink ownership patterns or add Rc to many places.) In GHC, type errors can be deferred until run time, until the point where the expression containing the type error is forced. If the expression containing the type error is never evaluated, the program works; if it is, you get an exception raised. This combines especially well with laziness: for example one can find the length of a list even though evaluating the element of the list would result in runtime exceptions. This is useful when I'm halfway through a large refactoring, when there are plenty of errors remaining, but I want to run my test suite to determine whether the parts without compiler errors actually pass their tests.
(Yeah, I'm the guy with the co-worker who kept turning off -Wall in C++ because "nobody has time for that".)