#BabelOfCode 2024
Week 7
Language: Haskell

Confidence level: Medium low

PREV WEEK: https://mastodon.social/@mcc/114308850669653826
NEXT WEEK: https://mastodon.social/@mcc/114463342416949024
RULES: https://mastodon.social/@mcc/113676228091546556

I was going to do Fennel this week, but then I looked at the problem and thought "this is ideal for Haskell "amb". I have been looking for an excuse to use Haskell "amb" for 25 years. So Haskell.

I have tried to learn Haskell 3 times now and failed. This "Babel of Code" thing was originally in part an excuse to do Haskell

I am not sure whether the reason I previously failed Haskell is

1. Because it's actually hard
2. Because of a mental block caused by failing at it more than once already
3. Because Haskell users are really bad at explaining things

I think it's a little 2 and mostly 3. I *love* ML, I know two MLs (3 if you count Rust) plus have in the past written my own ML. I understand the parts of Haskell that are just ML and get lost whenever I hit "do"— the point of divergence from ML; the dreaded Monad.

Question: The Haskell 2010 documentation describes its basic I/O functions, somewhat ambiguously, as "character oriented". I assume this means "ASCII character oriented". Is there a way in Haskell to get equivalents of getChar, putChar, string operations etc which are *UTF-8 character* oriented? I don't need graphemes, I'm happy with codepoint resolution.

In the Haskell docs

https://wiki.haskell.org/Haskell_in_5_steps

It states this is how you build a Haskell program to run it.

Assuming I realize I can drop -threaded, is actually the easiest/correct way to build a Haskell program to run in the year 2025?

Haskell in 5 steps - HaskellWiki

I run the given ghc --make command. It leaves some crap in src/. Say I do not want intermediate files in my source tree. I would like them to be moved to bin/ or obj/ or something, or simply not retained. Is this possible, or is Haskell in 2025 simply a "leaves crap in src/" kind of language in 2025?

I found -no-keep-hi-files and -no-keep-o-files (despite them technically not being documented) but say I want to retain them, just in a place of my choosing.

Welp, after 25+ years of trying, I have written my first working Haskell program. It reads one line from stdin and then prints it back out. I have now finally used a "monad", although I still don't feel I know what one ~is~.
I am trying to switch my program to Cabal-driven builds. This is one of the questions it asks you when you run `cabal init`. I think I understand what it is about the Haskell community that lead them to do it this way, but in my opinion, this is bad user experience. If the newest version of the Cabal format isn't the recommended one then why did you release it at all?

I am attempting to call "openFile" on the first command line argument in Haskell¹. It doesn't like it.

I'm not sure I'm looking at the right docs. I searched Google for "haskell system.io" and got https://hackage.haskell.org/package/base-4.21.0.0/docs/System-IO.html#v:openFile . I don't know if this is the newest Haskell2010 or if $GOOG is confused.

The doc (every doc I find) claims the type of openFile is FilePath -> IOMode -> IO Handle. But hls on my computer seems to think it's FilePath ->IOMode -> Bool -> IO Handle. Am I missing something?

System.IO

Answer to my previous question was I naively took the first suggestion from hls and imported GHC primitives where I should have imported System.IO. Cool. Works now

Haskell people, please help me.
There are 3 image attachments to this post, showing the same code block but with different amounts of indentation.

The first code block works,
the second block does not work,
the third one REALLY does not work.

According to my editor, none of these blocks of code contains tabs.

Haskell appears (?) to treat three spaces, four spaces, and eight spaces radically differently.

I dislike multiple-of-3 indents.

What do I need to read to understand what I am missing?

Another cursed question.

See attachment 1. This code compiles.

Reading the documentation ( https://hackage.haskell.org/package/megaparsec-9.7.0/docs/Text-Megaparsec-Char.html#v:space ), I realize I do not want space but "space1" (see attachment 2).

I change the symbol "L.space" to "L.space1". No!! Says GHC. L does *not* export space1!! only space!!

But the documentation says it exports space1?

Text.Megaparsec.Char

My problem can be explained if when I put dependency "megaparsec ^>=9.7.0" in my cabal file it picked like version 5 or 6 or something.

Is there a way to get cabal to print out for me what version it actually chose of each solved dependency? In npm or Rust for example I would consult the lock file.

I never solved the space1 problem but worked around it with a solution from @dysfun . I now have three new questions.

1. In attachment 1, why is "return" not required on L.decimal? I originally wrote "return" and it gave me a hint saying I could remove it, and removing it works. But return *is* required on (lsum, nums)?

2. In attachment 2: If att. 1 is allowed, why is this not allowed? It gives "parse error (possibly incorrect indentation or mismatched brackets)" on the _. Wrong type syntax?

I went from OCaml to Rust and coming back to Functional Land, one thing I'm really noticing is just *how much fricking better* the Rust error messages are than OCaml's, and consequently, how much fricking better they are than Haskell's. One thing is since Rust has less extensive type inference, you get way less "spooky action at a distance" in Rust than OCaml/Haskell and thus errors tend to actually be marked at the site where they really occur.
I've been having extensive problems in my program using the symbol "spaceChar" exported from Megaparsec, because if I use spaceChar in an expression, but the *variable to which the expression which uses spaceChar is assigned* is unused, everything breaks (and the error is inscrutable). OCaml is a spooky language but I never saw anything THIS spooky happen. Writing normal idiomatic code, too much wound up implicit and the compiler cannot even explain to me what it is that it doesn't understand.

One more question (I think this question might be outright goofy).

Is there a specific syntax for "calling a monad of a different type" from the current monad?

I have constructed a megaparsec-monad combinator that parses my pattern. I've made an IO-monad function that reads lines one at a time. If I call the megaparsec combinator I made inside my IO monad, I get a confusing error.

The megaparsec tutorial implies a combinator can act like a function that takes strings: https://markkarpov.com/tutorial/megaparsec.html#forcing-consumption-of-input-with-eof

Megaparsec tutorial

Haskell Programmers Will Literally Write Multiparagraph Comments Instead Of Just Giving The Parameter A Name Longer Than One Letter

So the answer to my last question was to use "parse"/"runParser" (aliases for 1 function) from megaparsec. Great.

It's not working and I think the problem is I don't understand destructuring. I want the equivalent of Rust

let Some(x) = f() else { panic!("Not found!"); }

I *think* I'm getting an Either, and I need to match its cases. But the way I know how to do that is "case…of". And the arrows in that "point the wrong way"?? Compare this similar attempt to unpack a list into its 1 item:

Alright. Thanks for the explanations y'all. I am now correctly parsing my input file. Current source:

https://github.com/mcclure/aoc2024/blob/d23ee9139e6645022504fce2dc26f39601e66933/07-01-operations/app/Puzzle.hs

I STILL can't figure out how to make the match on line 43 give a more human-readable error (on the array destructure on the command line argument processing) than "user error (Pattern match failure in 'do' block at app/Puzzle.hs:43:5-12)", but since I'm the only one running this I guess this only matters for "this hurts my sense of professionalism!!" reasons.

aoc2024/07-01-operations/app/Puzzle.hs at d23ee9139e6645022504fce2dc26f39601e66933 · mcclure/aoc2024

Advent of Code 2024 challenge (laid-back/"babel" version) - mcclure/aoc2024

GitHub

I will say. I could have done *this entire 50 line Haskell program* in the following two lines of perl:

perl -e 'open(FH,"$ARGV[0]"); while (<FH>) { /^(\d+):\s+(\d+(?:\s+\d+)*)$/ or die "Invalid input"; my $sum=$1; my @‍ops = split(/\s+/, $2); }'

…and I suspect writing that not only required far, far less thought for me, but would have required far less thought for someone who was already versed in both Haskell and Megaparsec.

(EDIT: Note to run this snippet you must remove the 0-width space.)

@mcc is this just summing numbers from a file?
print . sum . fmap (read @int) . words =<< readFile . head =<< getArgs

@zmz @int No; it is splitting a file into lines, then for each line matching the pattern

num: num [num num…]
and returning the tuple (initial_number, [list_of_following_numbers])

So that is a little more complicated.

I also intentionally did it such that it interprets each line one by line instead of buffering the entire file (the perl oneliner solution does this) which might complicate things.

@mcc @int sorry, distracted and wasn't reading carefully. still pretty simple to do idiomaticly, but too much for my phone. readFile isn't lazy io, but there is a version that does lazy io that would make that steam the file

@zmz @mcc @int readFile is lazy

ghci> x <- readFile "test.hs"
ghci> writeFile "test.hs" "awaga"
*** Exception: test.hs: withFile: resource busy (file is locked)

(so is hGetContents, which it is)

@amy @mcc @int ahh, I was misremembering
@amy @mcc @int I think because we use Relude (standard library replacement) for most things at work and that exports `readFile :: FilePath -> Text` (although it actually doesn't anymore because that assumes utf8) that's probably where my brain went for readFile being strict

@mcc @zmz my username has landed me in a Haskell community discussion on regular expressions.

Wrong number. Figures.

@int @mcc Sorry 😅 @ is used is Haskell for type application (a way to tell the compiler that you want to use a polymorphic function at a concrete type)