I've dabbled with Haskell. I like it a lot, but I want to like it more and I just can't. I don't come from a CS background, I'm pretty good mathematically, and the syntax is just beautiful to me. I remember when I first understood recursion instead of loops, I was amazed.
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'm pretty good mathematically, and the syntax is just beautiful to me.
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.
Hi, thanks for sharing this. I'm actively trying to understand the roadblocks to onboarding new users to Haskell, and trying to fix them.
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.
Monads are easy. They're just a design pattern that provides a formal abstraction over operation sequencing with additional computation. If you just accept them as they are, rather than attempting to grok them by approaching them by analogy ("monads are like burritos"), you will make your life much easier.
That's the secret to Haskell. There's no "getting it". There's just using it.
I TA’ed a Haskell course for four years, and I came to the conclusion that what made it so difficult to learn was the apparent implicit mixup between newtype syntax / overloading and purpose of even using them. (The newtype syntax is necessary to differentiate between multiple instances of the typeclass; if you threw away the newtype syntax and overloading, >>= could only mean something for one thing, making it comprehensible but less useful — taking both steps at once loses most people.)
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!”
> They're just a design pattern that provides a formal abstraction over operation sequencing with additional computation.
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.
Try using it for concrete problems you have today, start small. I used it to write a small calorie counting web app and some other tiny projects and picked it up that way.
Well, I wrote a series of functions doing a bunch of mathematical calculations, making lists and that. But when I went to 1) read a file, 2) output results to a file, I ran into some trouble. This was for a real use case I had. Writing the code to do all the calculation was really enjoyable and pretty easy to pick up. Actually turning it into a useful program not so much.
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.
It's normal to hit some walls, usually from lack of/difficulty in finding the right kind of intro material. (For example, with all respect to my esteemed sibling poster, their advice is overcomplicated.) I encourage you to try again. Laziness can get in the way of I/O, especially in small interactive programs. And Haskell can be written incrementally, once you know a few tricks. We'll be happy to help with "useful program" tips in chat (http://matrix.to/#/#haskell:matrix.org).
For reading line by line from stdin you can do something like
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.
> When I went in stackexchange and other community spaces I got similar results
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.
When you grok the IO type ("monads") it stops feeling like jumping through hoops, and then it isn't any harder to write imperative code in it than in any normal, imperative language.
Haskellers think I/O is an important effect and want to manage when it's allowed. It's allowed in the IO context, which happens to be the default:
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)
Because everything can do IO and launch missiles there :) And there's no way to limit it (except maybe algebraic effects introduced recently? I'm pretty sure nobody uses them anyway)
shoot me an email, which is on my profile. I can help. I honestly was where you were a few years ago, and worked through it.
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.
You might benefit from this video [1] on the "Functional Core, Imperative Shell" pattern in Ruby (a very object-oriented language), which sounds like one of the things you are having trouble with.
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:
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.
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.