Writing a "purely" side effect-based one, say, read a string from stdin, parse an age, and say, "What, you are %d years old?!?! Wow, you're old!" completely eluded me.
Maybe it was just that all the tutorials I encountered sucked. Maybe I am just too dumb for Haskell. Given that I currently really love Go, I kind of suspect the latter. But who knows? I'll find out next year, I guess. ;-)
main :: IO ()
main = do
putStrLn "Please input your age:"
age <- getLine
when (age < 10) (putStrLn "..." ++ (show age))
when (10 =< age < 40) (putStrLn "..." ++ (show age))
when (age >= 40) (putStrLn "..." ++ (show age))
But as you get more comfortable, you get something like: type Age = Int
-- Imagine there are constants here for youngAgeMessage, middleAgeMessage, and oldAgeMessage,
-- for simplicity they're just strings, though you could add even more descriptive aliases
-- like "OldMessage" with a quick union type like AgeMesssage = YoungAgeMessage | MiddleAgeMessage | OldAgeMessage, etc etc
makeAgeMessage :: Age -> String
makeAgeMessage age
| age < 10 = youngAgeMessage ++ show age
| 10 =< age < 40 = middleAgeMessage ++ show age
| age >= 40 = oldAgeMessage ++ show age
main :: IO ()
main = putStrLn "Please enter your age:"
>> getLine
>>= putStrLn . makeAgeMessage . read
This isn't even the final form of this code either, there are some more things you could do to make this code more axiomatic haskell. This code is approximate (like you probably can't copy and paste it, probably won't compile) but should at least show what I mean.It's similar to early clojure and the use (and eventual love, usually, of the threading macro "->") and the unix philosophy -- once you start writing those clean functions that pass whatever they need right along, it starts getting easier (and more desirable) to pluck out the parts that don't have to be side-effecting.
Also, the type system expresiveness is just amazing -- it's what Java should have been but never got the chance to be:
data Thing = OneThing | AnotherThing | ThirdThing
Guess what a `Thing` can be? literally just those things, not even a null or anything. A lot of people say they really love/need/only use "strongly typed" languages (which means a ton of things to a ton of people), but the biggest slap in the face to me is how a language like Java (that people will reach for when they want to compare some usually weaker language to a "strongly typed" one), is a literal minefield of Null Pointer Exceptions (NPEs). "Anything can be anything or sometimes nil" is a hard pill to swallow once you've used Haskell. Also, trying to use Option<> everywhere feels wrong if you do it in Java, because it starts to bleed everywhere, but actually... it's absolutely right (IMO) -- imagine how much better java code could be if the default was to Option.map over things, and the second you decided to try and pull a value out, you knew you were opening yourself up to something bad, instead of just passing things around and hoping they're there or writing repetitive checks.Now, if you see a `Maybe Thing`, you instantly know that that thing is either JUST the thing, or it's Nothing, and maybe's type is:
data Maybe a = Just a | Nothing
Type classes are also amazing, they're basically just as ergonomic as Go's interfaces, without some of the weird hangups and interface{}All that said, I definitely get the hangup, Haskell is difficult to get started with, even to this day, but man, that hurdle is so worth. Maybe I'm just addicted to high-learning-curve things but Haskell feels good.
Just one thing that stands out to me:
> "Anything can be anything or sometimes nil" is a hard pill to swallow once you've used Haskell.
In a language like Java yes, nearly every type is inhabited by null. In fairness, a similar problem exists in Haskell: every type is inhabited by "bottom" which is the computation that never terminates. This is a side effect of being lazy by default.
I've heard that Idris is basically Haskell but strict by default -- I've never tried it but maybe I should? I'm hesitant to go to a language with even less libraries/support/mindshare than Haskell though
import Text.Printf
main :: IO ()
main = do
age <- readLn
print (formatAge age)
formatAge :: Int -> String
formatAge age = printf "What, you are %d years old?!?!?! Wow, you are old" age
might look nice and imperative. But then you realize it is just syntactic sugar for `main = readLn >>= printf ...`, which is incredibly confusing when coming from another language. And then you learn that libraries can just invent their own meanings for `>>=` as long as the type signatures match and all bets are off.
The upshot of this is that the code like the one above is easy to test and could be async or parallelized without issue. The downside is that people usually fail several times before finally getting it.The haskell book[1] is apparently really helpful with this but it also costs 60$ and is over a thousand pages so it's a pretty large investment in both time and money.
For a really good book on programming, I'll happily pay 60 bucks. I will put that on my Christmas wish list, and give it another try next year.