#BabelOfCode 2024
Week 7
Language: Haskell
Confidence level: Medium low
PREV WEEK: https://mastodon.social/@mcc/114308850669653826
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. Part of why I started this BabelOfCode project in the first place was to have an excuse to do a Haskell week
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.

@mcc the overcautious approach this language has towards causally chained operations with side effects is what put me off.
from my own compiler building i know that it is *so easy* on the implementation side to first chain ops based on an implicit time order, then later remove all obvious dependencies that don't actually matter (which is easy to prove), which leaves only the monadic dependencies. all optimizing imperative compilers implement this informally in some way.
@mcc the character is based on the locale by default. https://hackage.haskell.org/package/with-utf8-1.1.0.0/docs/System-IO-Utf8.html looks like a good option
@mcc I hesitantly admit I am curious to learn haskell but I have a countdown that has to expire first and that countdown resets any time someone is weird to me about haskell. I've also got one of these for undertale.
@mcc I've also got one for Rust but it's barely worth mentioning until all the ex-haskell people move on to some new shiny thing
@aeva I am actually partway into a blog post that I would summarize as "Here's how to start with Rust without being required to like Rust". It was partially inspired by your Difficulties engaging with the (admittedly in its specific ways very frustrating) community
@mcc yeah,I know what you mean. It’s almost as though knowing Haskell means you forget what not knowing Haskell was like.
@holdenweb I think one of the hardest and most important problems in computer programming is learning to think outside of ourselves. After all, almost every single one of us is writing software with the goal of a person other than ourselves actually using it.
I'm kinda curious as to trying out Rust some day, but there's a bunch of reasons that have kept me away... one of them is the community (and also the desire to rewrite everything).
Others are things that have come from needing to build packages that use rust -like the compiler being so heavy on RAM (and slow) and it being a hassle to make cargo not use the internet during build time (to the point that I gave up on doing that... it is possible, but yeah.)
And also somehow being unable to do proper dynamic linking kinda bothers me...
Oops, didnt really mean to rant, just to say that yes I also have difficulties.
@urja The fact that the build tools are so vague about whether they will/won't work in offline mode, and what they *do* if they are allowed to run in online mode, is indeed a frustration.
I find the lack of dynamic linking more forgivable solely because frankly, it's never worked properly for me in C/C++, so I'm not sure I'm giving up something I actually had.
@mcc hence the need foff ref user (or market) research. Helps avoid building systems nobody wants that some nob thought would be a great idea. For example😉
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?

@mcc I love monads (I don't) and the thing I love about them is I believe it is part of the definition, that they cannot be defined in any way that can be understood...
I've seen c++ takes on them, and those are really easy to understand, which I believe means they are, by definition, not monads.
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.

@mcc -odir and -hidir for where to put the files. i think --make and -threaded are there by default today
@mcc I like having my code in a cabal project and then I can just specify all those flags in my `.cabal` file and only do `cabal run name -- clioptions`. All these intermediate build steps get taken care of.
@mcc Isn't that like rustc/cargo, most people using cargo instead of rustc directly? 🤔
@mcc i guess the "normal" way to use haskell is exclusively via cabal which puts all the output files elsewhere
@annanannanse Wait, what? I can use cabal for normal builds?
Are these the directions to use ?
@mcc researching this online I found a build tool called cabal.
cabal build --builddir=dist/build-artifacts
@lambdatotoro Thanks. Can a cabalproject define multiple executables in one project?
@mcc You can specify that to ghc by hand but most Haskell tooling expects you to build using cabal in the same way that most Rust tooling expects you to build using cargo.
If you don't want to, cabal build --verbose tells me it uses '-outputdir', '-odir', '-hidir', and '-stubdir' to keep the source directory clean.
@mcc Absolutely! At work I have a project with three executables and two test suites all in one.
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~.

@mcc i'm afraid i'm not up to speed on howto cabal best (i only use haskell via a custom build system that also just calls ghc...), but you need a .cabal file defining your exe and the `cabal build/run` should work.
@aj By more efficient you mean, it has some sort of lookaside for indexed access, or what?
@mcc I heard they are like burritos
@onelson I heard that too but it doesn't help me
Monad monoid endofuctors etc
@mcc yeah
@mcc @annanannanse You need a cabal project to be able to use `cabal build` or `cabal run`. There is `cabal init` to produce a skeleton. Not so simple unfortunately.
I think for simple tasks it is better to run stuff from ghci and just forego compiling entirely.
@mcc at one point I understood Monads by implementing Deferred (which everyone kept telling me was actually a Monad) in Haskell. As I recall, I started with about 700 lines of code and eventually got it down to 0 as I finally, fully realized the idea that it was a monad. But the experience of learning Haskell this way felt like ascending in a roguelike; I accomplished something, but I could not take it with me. Today, I do not know what a Monad is and I cannot write Haskell.
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?
![Please choose version of the Cabal specification to use:
1) 1.24
2) 2.0 (support for Backpack, internal sub-libs, '^>=' operator)
3) 2.2 (+ support for 'common', 'elif', redundant commas, SPDX)
4) 2.4 (+ support for '**' globbing)
* 5) 3.0 (+ set notation for ==, common stanzas in ifs, more redundant commas, better pkgconfig-depends)
6) 3.4 (+ sublibraries in 'mixins', optional 'default-language')
Your choice? [default: 3.0]
Please choose version of the Cabal specification to use:
1) 1.24
2) 2.0 (support for Backpack, internal sub-libs, '^>=' operator)
3) 2.2 (+ support for 'common', 'elif', redundant commas, SPDX)
4) 2.4 (+ support for '**' globbing)
* 5) 3.0 (+ set notation for ==, common stanzas in ifs, more redundant commas, better pkgconfig-depends)
6) 3.4 (+ sublibraries in 'mixins', optional 'default-language')
Your choice? [default: 3.0]](https://files.mastodon.social/media_attachments/files/114/433/806/943/044/832/original/560cf58ba4e46193.png)
Getting more serious about what a monad is
@onelson @mcc I think analogies just don't really work at all, whether to abstractions like food or to other different sorts of languages.
Imo the useful definition of a monad is just that it is a particular kind of nested data structure that happens to be useful for describing computation constructively. A math nerd will tell you that's not all it is, but from a programmer's perspective that's why it matters.
@aj the file it generated has what appears to be hundreds of lines of comments actually
@mcc Congratulations!
@mcc I suspect monads are a prank and most of the people claiming to understand them are not in on it
@mcc I'm not sure I completely understand them, but what I learned is: You can't have side effects, so if you need to do something which inherently has a side effect (e.g. print something to a terminal, or make a network request), you fix it by putting the side effect in a box which you can pass around. The box can be treated like a normal value from the outside. You can transform the stuff inside the box to something else, but it always has to stay inside the box.
@mcc it is just a construct that lets you sequence computation with `>>=`. if you check the type of that you will see it is just continuation passing style!
you normally use 'do' statements which desugar to `>>=` (when using `<-` to bind something) or `*>` (when not). and then you can just think of newlines as 'and then'
@mcc Maybe they liked the python2/python3 schism so much they would like to see if they can get that for their community?
@mcc LTS and supporting multiple versions of GHC in the wild... 😮💨
Debian stable is at GHC 9.0.2, Debian unstable at GHC 9.6.6 and upstream GHC is maintained at versions 9.6 up to 9.12. I have seen some good work from Cabal devs, but feature sets may be more practical to have packages available over a broad range of versions. I totally agree and would want new packages to take up the new features, though!
@mcc there are two reasons for this:
1. haskell is lazy, therefore we need a way of sequencing things like I/O
2. the monad abstraction is one-way. it's very easy to embed something in a monad and there is no way to get it out in general (though for many monad types such as lists, you can)
@mcc To run your program, you can just do
```
runhaskell A.hs
```
If you want the binary, you can do
```
ghc A.hs
```
Which should create a binary A;
```
./A
```
@mcc `do` is a DSL for working with Monads; using `do` is never required, you can always use plain operators. Admittedly, the same code is usually easier to read and understand in the `do` DSL.
@mu Oh, is that the idea? That 3.0 is LTS?
@mcc Are you open on feedback wrt do / monadic code?
@clementd In about an hour I will be but right now I'm just trying to find my way around the reference docs and don't have headspace to make sense of whatever you say. Thanks for asking
@mcc Sure!
@0xabad1dea @mcc this made me research it. I return to say that I still only half understand it, but if someone asks I'm just going to tell them it's an Optional to wrap nullable values.
@TuffyPuff @0xabad1dea My understanding is "monads are generalized Option" is a statement in-the-know Haskell users would agree with.
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?
![import System.Environment
import GHC.IO.FD (openFile)
import GHC.IO.IOMode (IOMode(ReadMode))
main :: IO ()
main = do
[inFile] <- getArgs
file <- openFile inFile ReadMode import System.Environment
import GHC.IO.FD (openFile)
import GHC.IO.IOMode (IOMode(ReadMode))
main :: IO ()
main = do
[inFile] <- getArgs
file <- openFile inFile ReadMode](https://files.mastodon.social/media_attachments/files/114/433/945/734/278/285/original/669cf9160097df9d.png)

@mcc you're looking in the wrong package https://hackage.haskell.org/package/base-4.21.0.0/docs/System-IO.html#v:openFile
@mcc any particular reason why you're importing GHC.* and not System.IO?
@mcc i'll just leave this link here for next time https://hoogle.haskell.org/
@bars Because it's what hls recommended. Thank you
@dysfun Thanks
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
@mcc Sure. Keep us posted!
@mcc it (what a monad is) clicked for me when I was working with Clojure for a few months. In a “okay, that *generalised sort of thing*, okay, right. ” sort of way. Aaand it’s fully evaporated from my brain now. Totally absent.
It turns out that I find the aspects of FP that you are forced into by using a LISP based language good and useful, and none of that makes the theoretical stuff any less impenetrable or frustrating.
@mcc trying to learn Haskell just made me tired
@mcc I think it's a bit conservative for that. Cabal 3.0 is relatively old, and I suspect the choices in cabal init might not be up to date.
@mcc I am excited you’re writing Haskell. 😄
In general, it’s unlikely you want to be importing `GHC`. If ever you do need to, you’ll know.
@mcc would you be interested in a video chat intro sometime tomorrow, east coast USA biz hours?
I got paid to write Haskell for five years, could maybe speed up your getting started process?
@shapr That is a very generous offer, at the moment I don't think it's needed but thank you.
@mcc As a Haskell enjoyer, I have no qualms in saying that neither cabal nor stack *feel* good to use, and I fight each of them at different junctures.
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?



@mcc haven't done Haskell in a while, but I don't think it cares how many spaces, just that they're the same. Your problem is that the alignment point is the start column of the first token after the `do`.
@kw217 oh no. okay, I see :(
@mcc only in the first case is the "let" keyword (and following code) aligned to "do"
It's just significant whitespace. You can either align everything on the column after "do " (that's where the 3 chars come from) or indent on a newline with an indent width of your choice.
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?
![import System.Environment
import System.IO
import Data.Void
import Text.Megaparsec
import Text.Megaparsec.Char
import qualified Text.Megaparsec.Char.Lexer as L
import Control.Monad (void)
type LineParser = Parsec Void String
parseLine :: LineParser (Int, [Int])
parseLine = do
let space = L.space (void spaceChar) empty empty
sum <- L.decimal
_ <- char ':'
_ <- space
_ <- eof
return (sum, []) import System.Environment
import System.IO
import Data.Void
import Text.Megaparsec
import Text.Megaparsec.Char
import qualified Text.Megaparsec.Char.Lexer as L
import Control.Monad (void)
type LineParser = Parsec Void String
parseLine :: LineParser (Int, [Int])
parseLine = do
let space = L.space (void spaceChar) empty empty
sum <- L.decimal
_ <- char ':'
_ <- space
_ <- eof
return (sum, [])](https://files.mastodon.social/media_attachments/files/114/434/634/632/378/528/original/83a0fff4b5d9e807.png)

![app/Puzzle.hs:13:17: error: [GHC-76037]
Not in scope: ‘L.space1’
NB: the module ‘Text.Megaparsec.Char.Lexer’ does not export ‘space1’.
Suggested fix:
Perhaps use ‘L.space’ (imported from Text.Megaparsec.Char.Lexer)
|
13 | let space = L.space1 (void spaceChar) empty empty
app/Puzzle.hs:13:17: error: [GHC-76037]
Not in scope: ‘L.space1’
NB: the module ‘Text.Megaparsec.Char.Lexer’ does not export ‘space1’.
Suggested fix:
Perhaps use ‘L.space’ (imported from Text.Megaparsec.Char.Lexer)
|
13 | let space = L.space1 (void spaceChar) empty empty](https://files.mastodon.social/media_attachments/files/114/434/639/513/146/200/original/9dd728488fd32992.png)
@mcc yeah okay that is cursed. and given that the thing above it has been available since 3 major versions after it a version issue seems unlikely
@dysfun My import says " megaparsec ^>=9.7.0 " and I don't know what version I have
@mcc well that's definitely more than 6.0
@mcc the hints in the documentation for that function may provide a useful workaround, however
@froufox How do I tell what version I have?
My import in the cabal file says " megaparsec ^>=9.7.0 "
@dysfun How so?
@dysfun Hm. That's true. But not, afaict, hspace1 :(
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.
@mcc well skipSome will still work, you just need to replace spaceChar. looking at the code for space1 and hspace1 i think this shouldn't be too hard https://hackage.haskell.org/package/megaparsec-9.7.0/docs/src/Text.Megaparsec.Byte.html#space1
@mcc cabal list --installed ?
@mcc https://hackage.haskell.org/package/megaparsec-9.7.0/docs/src/Text.Megaparsec.Byte.html#spaceChar
`hspaceChar = satisfy isHSpace` ?
@dysfun This did something I do not understand.
https://gist.github.com/mcclure/a189ef177437f8de04a4bf674fa42747
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?
![spaceThenNumber :: LineParser Int
spaceThenNumber = do
let spaces = L.skipSome spaceChar
_ <- spaces
L.decimal
parseLine :: LineParser (Int, [Int])
parseLine = do
lsum <- L.decimal
_ <- char ':'
nums <- many spaceThenNumber
_ <- eof
return (lsum, nums) spaceThenNumber :: LineParser Int
spaceThenNumber = do
let spaces = L.skipSome spaceChar
_ <- spaces
L.decimal
parseLine :: LineParser (Int, [Int])
parseLine = do
lsum <- L.decimal
_ <- char ':'
nums <- many spaceThenNumber
_ <- eof
return (lsum, nums)](https://files.mastodon.social/media_attachments/files/114/434/733/603/884/702/original/d4f8803071a78c4a.png)
![parseLine :: LineParser (Int, [Int])
parseLine = do
lsum <- L.decimal
_ <- char ':'
spaceThenNumber :: LineParser Int
let spaceThenNumber = do
let spaces = L.skipSome spaceChar
_ <- spaces
L.decimal
nums <- many spaceThenNumber
_ <- eof
return (lsum, nums) parseLine :: LineParser (Int, [Int])
parseLine = do
lsum <- L.decimal
_ <- char ':'
spaceThenNumber :: LineParser Int
let spaceThenNumber = do
let spaces = L.skipSome spaceChar
_ <- spaces
L.decimal
nums <- many spaceThenNumber
_ <- eof
return (lsum, nums)](https://files.mastodon.social/media_attachments/files/114/434/739/245/671/793/original/657b76561837d47a.png)
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 with 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 unused, everything breaks (and the error is inscrutable). OCaml is a spooky language but I never saw anything remotely 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.


![app/Puzzle.hs:32:41: error: [GHC-83865]
• Couldn't match expected type: String -> IO (Int, b0)
with actual type: ParsecT
Void String Data.Functor.Identity.Identity (Int, [Int])
• The function ‘parseLine’ is applied to one value argument,
but its type ‘ParsecT
Void String Data.Functor.Identity.Identity (Int, [Int])’
has none
In a stmt of a 'do' block: (lsum, operands) <- parseLine inStr
In the expression:
do inStr <- hGetLine inHandle
(lsum, operands) <- parseLine inStr
takeLine inHandle (acc + lsum)
|
32 | (lsum, operands) <- parseLine inStr
| ^^^ app/Puzzle.hs:32:41: error: [GHC-83865]
• Couldn't match expected type: String -> IO (Int, b0)
with actual type: ParsecT
Void String Data.Functor.Identity.Identity (Int, [Int])
• The function ‘parseLine’ is applied to one value argument,
but its type ‘ParsecT
Void String Data.Functor.Identity.Identity (Int, [Int])’
has none
In a stmt of a 'do' block: (lsum, operands) <- parseLine inStr
In the expression:
do inStr <- hGetLine inHandle
(lsum, operands) <- parseLine inStr
takeLine inHandle (acc + lsum)
|
32 | (lsum, operands) <- parseLine inStr
| ^^^](https://files.mastodon.social/media_attachments/files/114/434/756/246/601/721/original/3a9d3fc1e7ef1a73.png)
@mcc Something to do with monads? The do-block does something similar to the transform from async/await to callbacks and maybe you're doing the equivalent of dropping a future instead of awaiting it? But those error messages tell you none of that.
@mcc i'm guessing L.skipSome is a`LineParser ()`, thus you must sequence it monadically. that means you can't assign the result to a let.
@dysfun But can't I assign the monad to a let if I do not then *call it*?
@dysfun That is, I don't want to invoke it on that line. I just want to define a name.
@mcc oh i see. yes, you can do that.
L.decimal is a `Parser Int`. the rule is that the return of a do block must be a `m a` for some monad m. m here is Parser, a is Int, thus it's already an m a. But return would let you take say the integer 1 and get a `Parser a`.
@dysfun This is a very clear response, thank you.
@mcc return is a function btw, not a keyword. it is an alias of `pure`, which i think is a better name
@mcc as for the other thing, i'm a bit out of haskell these days, but maybe it works if you remove the _ <- from the left?
if there is no value to bind, you do not need the left arrow. i hypothesise it's upset that you used anyway for some reason
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
![import System.Environment
import System.IO
import Data.Void
import Text.Megaparsec
import Text.Megaparsec.Char
import qualified Text.Megaparsec.Char.Lexer as L
type LineParser = Parsec Void String
spaceThenNumber :: LineParser Int
spaceThenNumber = do
_ <- hspace1 -- Commenting this line out breaks it
L.decimal
parseLine :: LineParser (Int, [Int])
parseLine = do
lsum <- L.decimal
_ <- char ':'
nums <- many spaceThenNumber
_ <- eof
return (lsum, nums)
-- Read a line, parse it to get a number, add it to total so far
takeLine :: Handle -> Int -> IO Int
takeLine inHandle acc =
do inEof <- hIsEOF inHandle
if inEof
then return acc
else do inStr <- hGetLine inHandle
(lsum, operands) <- parseLine inStr
takeLine inHandle (acc + lsum)
-- Initial case for takeLine
takeLines :: Handle -> IO Int
takeLines inHandle = do takeLine inHandle 0 import System.Environment
import System.IO
import Data.Void
import Text.Megaparsec
import Text.Megaparsec.Char
import qualified Text.Megaparsec.Char.Lexer as L
type LineParser = Parsec Void String
spaceThenNumber :: LineParser Int
spaceThenNumber = do
_ <- hspace1 -- Commenting this line out breaks it
L.decimal
parseLine :: LineParser (Int, [Int])
parseLine = do
lsum <- L.decimal
_ <- char ':'
nums <- many spaceThenNumber
_ <- eof
return (lsum, nums)
-- Read a line, parse it to get a number, add it to total so far
takeLine :: Handle -> Int -> IO Int
takeLine inHandle acc =
do inEof <- hIsEOF inHandle
if inEof
then return acc
else do inStr <- hGetLine inHandle
(lsum, operands) <- parseLine inStr
takeLine inHandle (acc + lsum)
-- Initial case for takeLine
takeLines :: Handle -> IO Int
takeLines inHandle = do takeLine inHandle 0](https://files.mastodon.social/media_attachments/files/114/434/824/845/488/883/original/cbfb04407845053c.png)
![app/Puzzle.hs:30:41: error: [GHC-83865]
• Couldn't match expected type: String -> IO (Int, b0)
with actual type: ParsecT
Void String Data.Functor.Identity.Identity (Int, [Int])
• The function ‘parseLine’ is applied to one value argument,
but its type ‘ParsecT
Void String Data.Functor.Identity.Identity (Int, [Int])’
has none
In a stmt of a 'do' block: (lsum, operands) <- parseLine inStr
In the expression:
do inStr <- hGetLine inHandle
(lsum, operands) <- parseLine inStr
takeLine inHandle (acc + lsum)
|
30 | (lsum, operands) <- parseLine inStr
| ^^^^^^^^^^^^^^^
app/Puzzle.hs:30:41: error: [GHC-83865]
• Couldn't match expected type: String -> IO (Int, b0)
with actual type: ParsecT
Void String Data.Functor.Identity.Identity (Int, [Int])
• The function ‘parseLine’ is applied to one value argument,
but its type ‘ParsecT
Void String Data.Functor.Identity.Identity (Int, [Int])’
has none
In a stmt of a 'do' block: (lsum, operands) <- parseLine inStr
In the expression:
do inStr <- hGetLine inHandle
(lsum, operands) <- parseLine inStr
takeLine inHandle (acc + lsum)
|
30 | (lsum, operands) <- parseLine inStr
| ^^^^^^^^^^^^^^^](https://files.mastodon.social/media_attachments/files/114/434/841/305/113/031/original/72691f84b0b9935e.png)
@dysfun okay. so if I Monad there, it won't care the value is thrown away, the value just gets thrown away by default?
@mcc if the value is `()`
@dysfun I'm pretty sure the value is a value tho, not ()
@mcc oh, in which case nevermind.
@mcc monads have a way of running them that varies depending on the monad. in the case of megaparsec this is called `parse`. or there's another one `runParser`
@mcc In both Haskell and ML, it can help to declare types even when it's not required. Doing so acts as a static assertion that the value is indeed that type.
I try to do this for all top-level declarations, at a minimum. This really helps narrow down where the error actually happens.
@mcc you're interested in "lift IO" or "parser combinators"
Haskell Programmers Will Literally Write Multiparagraph Comments Instead Of Just Giving The Parameter A Name Longer Than One Letter


@mcc would now be a good time to point out how very blessed you are by megaparsec's fantastic documentation? rest assured this is not illustrative of the average
@dysfun Yes, in fact this is why I'm using megaparsec and not something else >_>
@mcc Agree, Haskell's error messages are some of the worst I've ever had to deal with. I think Elm, despite having a similar level of inference, did error messages quite well.
@mcc There's some library, I think it's one of the Lens ones, where the type variables for a type spell "s t a b". It doesn't help me know what they mean or do, but it does give me a chuckle.
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:

![app/Puzzle.hs:30:41: error: [GHC-83865]
• Couldn't match type ‘Either (ParseErrorBundle String e0)’
with ‘IO’
Expected: IO (Int, b0)
Actual: Either (ParseErrorBundle String e0) (Int, b0)
• In a stmt of a 'do' block:
(lsum, operands) <- parse "DUMMY-FILENAME" parseLine inStr
In the expression:
do inStr <- hGetLine inHandle
(lsum, operands) <- parse "DUMMY-FILENAME" parseLine inStr
takeLine inHandle (acc + lsum)
In a stmt of a 'do' block:
if inEof then
return acc
else
do inStr <- hGetLine inHandle
(lsum, operands) <- parse "DUMMY-FILENAME" parseLine inStr
takeLine inHandle (acc + lsum)
|
30 | (lsum, operands) <- parse "DUMMY-FILENAME" parseLine inStr
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
And 2 more similar errors app/Puzzle.hs:30:41: error: [GHC-83865]
• Couldn't match type ‘Either (ParseErrorBundle String e0)’
with ‘IO’
Expected: IO (Int, b0)
Actual: Either (ParseErrorBundle String e0) (Int, b0)
• In a stmt of a 'do' block:
(lsum, operands) <- parse "DUMMY-FILENAME" parseLine inStr
In the expression:
do inStr <- hGetLine inHandle
(lsum, operands) <- parse "DUMMY-FILENAME" parseLine inStr
takeLine inHandle (acc + lsum)
In a stmt of a 'do' block:
if inEof then
return acc
else
do inStr <- hGetLine inHandle
(lsum, operands) <- parse "DUMMY-FILENAME" parseLine inStr
takeLine inHandle (acc + lsum)
|
30 | (lsum, operands) <- parse "DUMMY-FILENAME" parseLine inStr
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
And 2 more similar errors](https://files.mastodon.social/media_attachments/files/114/434/943/325/637/203/original/82d665eefd0f9d86.png)
![-- This next line COULD HAVE BEEN
-- [inFile] <- getArgs
-- and that would work, but crashes if it doesn't match.
-- I awkwardly replace that short thing with this longer thing,
-- and it doesn't work…
[inFile] <- case getArgs of
[inFile]
_ -> error "Expected exactly one argument" -- This next line COULD HAVE BEEN
-- [inFile] <- getArgs
-- and that would work, but crashes if it doesn't match.
-- I awkwardly replace that short thing with this longer thing,
-- and it doesn't work…
[inFile] <- case getArgs of
[inFile]
_ -> error "Expected exactly one argument"](https://files.mastodon.social/media_attachments/files/114/434/949/304/122/901/original/22589d573c4d0817.png)
![app/Puzzle.hs:45:9: error: [GHC-58481]
parse error (possibly incorrect indentation or mismatched brackets)
|
45 | _ -> error "Expected exactly one argument"
| ^ app/Puzzle.hs:45:9: error: [GHC-58481]
parse error (possibly incorrect indentation or mismatched brackets)
|
45 | _ -> error "Expected exactly one argument"
| ^](https://files.mastodon.social/media_attachments/files/114/434/950/205/228/512/original/2f40b87031207a67.png)
@mcc Realtalk: this one-letter-variable-name thing is one of the things that pushed me out of academia.
Read some professor's code. Realized not only was this fine, this was considered standard.
Decided I did not want to deal with this for the next forty years.
@mcc the argument order for parse is wrong. it take the parser first and the file second
@mcc for destructuring, if a pattern match fails in do notation it calles "fail" from MonadFail. for IO that throws an error. You could use a Monad Transformer over IO to get difference behavior or have the first case be
"[x] -> [x]"
@zmz I did try it with [inFile] -> [inFile] and the line after was still an indentation error.
@mcc return puts a value in the monadic context - it would make a `LineParser a` when you return any `a`. L.decimal is already a LineParser so you don't need that.
Alright. Thanks for the explanation y'all. I am now correctly parsing my input file. Current source:
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.
@mcc you can replace that with a case. you've just handled a case with monadic returns, even!
@mcc (that would be like `inFiles <- getArgs; case inFiles of ...`)
I will say. I could have done *this entire 50 line 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.
@mcc haskell is never going to compete with perl on concision. but you might feel more confident about the code after...
@dysfun Honestly, I *don't* feel confident about this code the way I do about Rust or Ocaml code. There is too much magic.
@mcc love perl's "or die"
@Norgg <Larry Wall> THEN PERISH
@mcc sure, but you already know rust and ocaml much better than you know haskell.
and yes, i know perl that well and i'd rather have the haskell one.
@mcc I kinda feel like I should go back to Perl. All I do is admin stuff anyway and I'm not sure anything else is really better for it.
@mcc @Norgg that'll be when you use autodie https://metacpan.org/pod/autodie
@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 I'm not a Haskell person, but did you know that an MCC is the South African name for Champagne? 🥂😜
I would think the recommended version would be older because the latest is still in testing... can you think of a better way to release (and publicize, and test) new features?
Anyways love how you're posting about this, I probably wouldn't get any responses if I did, but in going to do it anyway when I learn something new.
@mcc I haven't touched perl in at least 15 years and that example seems clear (it's a one-liner and therefore too terse). I can only imagine how alien and unreadable the Haskell version would be to me. Not that one-liner perl is any great prize but Haskell's syntax eludes me.
@arclight it's linked in the post before . You'll find it maybe not so bad because I don't use any of the . $ <*> << >> <* nonsense
@0xabad1dea @mcc For me the confusing part was that all the explanations mistakenly suggest that IO is made functional by monads. It is not; to my understanding IO is special cased in GHC and not actually functional. That it is a monad is also important, but not "the magic sauce".
@gd2 @0xabad1dea My understanding is monads are in this case a mechanism for *representing* IO functionally. The model allows it to make everything *except* the IO special casing functional, because your code remains pure transformations which are directed by the special-cased IO ordering.
over 40+ years I've had a go at a range of languages - I'm not talented but I'm not uncomfortable with different paradigms
I'm currently doing a course on haskell.
and your (3) reason is very valid imho
there seems to be a curse around haskell where the many courses, books, blogs, are just bad at explaining stuff
I can only say this with the confidence of having all those years behind me - otherwise I would be blaming myself
The Haskell documentation is clearly written for humans to read— *some* human, *somewhere*. It is definitely not written with the intent of *me* reading it.
![Creating computations
amb :: [b] -> AmbT r m b
Just for fun. This is McCarthy's amb operator and is a synonym for aMemberOf. Creating computations
amb :: [b] -> AmbT r m b
Just for fun. This is McCarthy's amb operator and is a synonym for aMemberOf.](https://files.mastodon.social/media_attachments/files/114/439/355/601/527/691/original/ac8a418dbecacc2a.png)
@mcc c'mon you're likely just 6 ICFP papers away from understanding a programming dad joke. :D
@mcc that's exactly how i imagine a Haskell developer having "fun"
Haskell syntax question: Is there a thing I can do to convince GHC that when I said "-1" I REALLY, definitely wanted the unary integer negation operator and something else, so I don't get a spurious warning
(No one agrees with me but I still think SML was right to use ~ for integer negation.)
![aMemberOf [-1, 1] comes with a long warning explanation saying "-1" is defaulted to "Integer" but only potentially aMemberOf [-1, 1] comes with a long warning explanation saying "-1" is defaulted to "Integer" but only potentially](https://files.mastodon.social/media_attachments/files/114/439/569/266/391/835/original/0fb44dca92cf2b34.png)
@mcc (- 1)
or negate 1
@amy Hmm, it still puts a warning on "negate". I wonder if there's some way to explicitly tell it which instance of Num I meant by 1?

@mcc oh right. sry i thought this was the other issue with the unary negation operator. times :: Int <- amb [-1, 1]
or [-1 :: Int, 1]
(or [-1, 1 :: Int]
etc)
@amy Thank you very much.
I've reached the part of the puzzle that inspired me to write this in Haskell to start with: The "amb" combinator. I read about this decades ago and it captivated me as the one example I'd seen of what made Haskell's weirdo pure-functional model "useful".
Here's my outsider's awareness of what a monad is:
You write some code, and it's executed in a sequence.
*What does that mean*?
The monad defines what *sequentiality* means. For example "IO" means "linearly in time, as external events allow"
(Okay functional fans, THIS is the point where you can reply to object I'm describing monads wrong.)
I think this is why the Haskell folks latch onto category theory. As best I understand, category theory is about explaining what it means to "apply" a thing to another thing:
https://web.archive.org/web/20250107162216mp_/https://cohost.org/mcc/post/75444-this-video-is-the-on
I think I understand why the correspondence from the category functor to the Haskell functor typeclass is exact, so I'm willing to believe Haskell Monads mostly correspond to…something categorical.
Anyway, the Amb monad, as offered by the "nondeterminism" package¹:
https://hackage.haskell.org/package/nondeterminism-1.5#readme
Stretches this definition by defining "sequential code" thus: *It is nonsequential*. The Amb monad can fork computations, and answer questions like "do all paths return?" "does ANY path return?" "give me a list of all possible results".
¹ For some reason until today I thought it was a language builtin. It's not; it's just something multiple people have implemented in various ways in the last 30 years.
@mcc The way I normally think of monads is as something that wraps a value in some way, but where you can still operate on that wrapped thing using functions that don't know about monads.
E.g.: async is a monad, but you can still do normal stuff to an async future by awaiting fist. Or list is a monad, but you can still do normal stuff to a list by mapping scalar functions over the list.
So here's a test program with Amb. You can probably read it without knowing Haskell! "aMemberOf" in sumTen forks the computation by running the code following it with *both* members of the list -1, 1.
We de-monadify this, making it a real single computation, with the nondeterminism package's "isPossible", which returns true if any path does. Does *either* 6 + 4, *or* 6 - 4, sum to ten? 6 + 4 does, so this code prints true. Wow! That was shockingly easy!
![import Control.Monad.Amb
sumTen :: Amb Bool Bool
sumTen = do
times <- aMemberOf [-1 :: Int, 1 :: Int]
return (6 + 4 * times == 10)
main :: IO ()
main = do
putStr (show (isPossible sumTen))
import Control.Monad.Amb
sumTen :: Amb Bool Bool
sumTen = do
times <- aMemberOf [-1 :: Int, 1 :: Int]
return (6 + 4 * times == 10)
main :: IO ()
main = do
putStr (show (isPossible sumTen))](https://files.mastodon.social/media_attachments/files/114/439/644/665/375/250/original/e60371fb12fef3ec.png)
@mcc The one being near and dear to my own previously held heart, of course, is that Qubit is a monad that says "hey, this sent a gate to a quantum processor or read a result out at some point," but you can still act classically on results that you get and can still make classical decisions about what gate to apply next.
@mcc no, categorical functors are not the same as haskell functors. haskell functors are endofunctors where categorical functors map between two different carrier types!
Then I try to make it *marginally* more complex and I run facefirst into the wall that is Haskell's baroque syntax.
It took me like… 15 minutes to figure out how to modify the type signature of sumTen to take 1 argument. I kept expecting it to be Amb Bool Bool Int but no it's Int -> (Amb Bool Bool). I *think* I now understand why it's the way it is, and I *think?* I also understand why TakeLines upthread returned "IO Int" and not "Int -> IO", but I'm not sure how I was supposed to have known it
![import Control.Monad.Amb
sumTen :: Int -> Amb Bool Bool
sumTen plus = do
times <- aMemberOf [-1 :: Int, 1 :: Int]
return (6 + plus * times == 10)
main :: IO ()
main = do
print (isPossible (sumTen 3))
print (isPossible (sumTen 4))
print (isPossible (sumTen 5))
import Control.Monad.Amb
sumTen :: Int -> Amb Bool Bool
sumTen plus = do
times <- aMemberOf [-1 :: Int, 1 :: Int]
return (6 + plus * times == 10)
main :: IO ()
main = do
print (isPossible (sumTen 3))
print (isPossible (sumTen 4))
print (isPossible (sumTen 5))](https://files.mastodon.social/media_attachments/files/114/439/686/756/782/081/original/5e19624c04c085e0.png)
(Note: If my current mental model of the syntax is correct, this actually isn't a case of Haskell's syntax being overly baroque but a case of it being extremely minimal and things just working out the way they do "by coincidence". I think the problem I'm having is that the code keeps *alternating* being overly smart [like the indentation rules, still which feel complex to me] with being overly bone-dry simple [meaning you get one character off and the compiler can't give you a helpful error].)
@mcc "Amb" means "with" in Catalan so I keep reading your posts that way. Works pretty well tbh.
@mcc in fairness 'layout' (that's the name for the indentation syntax) *is* quite complicated. most of the other incidental complexity with haskell syntax i feel is permitting defining infix operators.
@cford Interesting! It's supposed to be short for "Ambiguous".
@mcc I think you're right. Layout is smart, but type syntax is minimal and complex things happen by composing things, but basic rules are deceptively simple
@mcc It is correct that category theory is about composing functions and this means it works well when you want to reason about composing functions.
But there's also the part of category theory that's about what you *can't* do. For example consider a Haskell function f with signature:
f :: [t] -> [t]
It's a polymorphic function mapping lists to lists. It has to work for t of any type and that means the function *can't* examine the elements in the list. All f can do is rearrange and copy from its argument, and do the same rearrangement whatever types are in the list. This means we can immediately say a lot about f without looking at the code.
This corresponds very well to the notion of a natural transformation in category theory. The definition of a monad involves two natural transformations.
@mcc Probably there's a common etymology.
I’ve also seen it referred to as the “angelic operator”, as an angel will swoop in to make sure the right choice was made even retroactively.
I’ve often wondered — and please understand that I know fuckall about physics really — if quantum coherence works something like amb behind the scenes.
@mcc The confusion you mentioned seems to be tied to confusion wrt type parameters, I suggest getting comfortable with it first.
@mcc The syntax is terse and scales impressively well (I'm still impressed by it after all these years), so it's easy to thing it's magic or complex, but in the end it's quite regular. The hardest part is not going too fast
@clementd Coming from ML, the pattern matching syntax seems more than a little baffling
@clementd I've been trying since last night to figure out how to put a second case on this destructure (something like Rust "let [x] = an_array else { panic!("Incorrect array length"); }
@mcc Let only works for irrefutable patterns. You need `case` for multiple patterns
@mcc Patter matching itself is not that different imo. Maybe patterns within do blocks? But that's not exactly the same thing. Patterns on the LHS of an arrow are indeed syntactic sugar
@mcc A refutable pattern within a do block will indeed add a `MonadFail` constraint on the whole blocks. If you want to explicitly work with multiple patterns, you'll need `case of`
@mcc I feel like it's more interesting to study what an individual thingy that happens to be a monad does, on a case-by-case basis, than caring about what the super-abstract concept of a monad *is* in general.
Some monads are hacks that let you express something procedural/sequential in a purely functional language. OK, cool, that's useful!
But are monads somehow fundamentally about *sequentiality*? I dunno and I'm reluctant to look into it, because I have doubts about this being interesting.
@clementd Incidentally a MonadFail really is what I want, I just wanted to control *the error message* of the MonadFail.
@mcc You need an extra ,`do` after `<-`
@clementd I'm having a little bit of trouble also with the fact there seems to be implicit pattern matching in "defining multiple versions of" a function, which ML does a bit more explicitly with the | blocks. it's just a lot at once.
@clementd After which <-? Do you mean after ->?
@mcc Yes sorry.
@mcc Ha yes. That's super convenient but indeed that can be surprising
@mcc `case` blocks take regular expressions after `->`, so it would be a new `do` block
@clementd Thanks. However, adding the single "do" to that previous code results in an even odder error. It looks like the [inFile] <- getArgs was doing some sort of implicit path->string type conversion with the [inFile] -> version can't manage?
![app/Puzzle.hs:56:9: error: [GHC-83865]
• Couldn't match expected type: IO [String]
with actual type: [FilePath]
• In the pattern: [inFile]
In a case alternative:
[inFile]
-> do file <- openFile inFile ReadMode
total <- takeLines file
putStr (show total)
....
In a stmt of a 'do' block:
case getArgs of
[inFile]
-> do file <- openFile inFile ReadMode
total <- takeLines file
....
_ -> putStr "Bad Arguments"
|
56 | [inFile] -> do
| ^^^^^^^^
app/Puzzle.hs:56:9: error: [GHC-83865]
• Couldn't match expected type: IO [String]
with actual type: [FilePath]
• In the pattern: [inFile]
In a case alternative:
[inFile]
-> do file <- openFile inFile ReadMode
total <- takeLines file
putStr (show total)
....
In a stmt of a 'do' block:
case getArgs of
[inFile]
-> do file <- openFile inFile ReadMode
total <- takeLines file
....
_ -> putStr "Bad Arguments"
|
56 | [inFile] -> do
| ^^^^^^^^](https://files.mastodon.social/media_attachments/files/114/439/802/496/242/460/original/4f929c78f4bfb4c4.png)
@mcc `Path` is an alias for `String`, so there's no conversion happening, it's two names for the same thing. Here the issue is that you have a `Path` where it expects an `IO Path`
If I am compiling a Haskell 2010 program in GHC 9.6.7 on an AMD64 machine, and I multiply two Int numbers to produce a third Int (Int NOT Integer), and the answer would be larger than a 64 bit integer can hold,
What happens?
Silent overflow?
A thrown exception?
@mcc overflow
@amy Imagine I believe a computation will stay under 63 bits but can't prove this. What level of performance sacrifices am I making if i use Integer instead of Int from the beginning and it turns out the 63 bit threshold is never breached?
@mcc my intuition of the monad is that they describe fractals, i.e. any data structure that can reasonable nest itself inside it along with some data, like List of List of List
and this nestedness is useful because it gives us fearless composition. like if we have two parsers `p1` and `p2`, you can nest them in `Parser(p1, p2).join`, like the turtle of the turtle all the way down. `IO` I think is a mini-program-as-data.
https://eed3si9n.com/monads-are-fractals/
@mcc I am assuming "silent overflow". But, this is based on people from the "FP community" on comp.lang.lisp stating that wrapping around instead of silently going bignum was a Good And Proper Thing To Do, which pretty much destroyed any wish I ever had of going type-hard, because "wrap around" only makes sense for an "integer modulo ..." and then it should not be named "Int".
Come use Haskell: Once you can get it to work at all, time travel is free! But good luck getting it to work at all
Working code:
Things of note:
- Running a time-traveling computation across many parallel universes took 11 lines of code. Splitting a string on colons and then again on spaces took 30
- Keeping with the "hard things are easy, easy things are hard" rule, I switched this code from 64-bit to arbitrary-precision ints by find-replacing "Int" with "Integer".

@mcc you pulled out megaparsec to split space separated colon delimited strings?
I was really expecting to milk 5-6 more posts out of the process of learning to use "Amb" itself but in fact there was just not much to say! Once I figured out how the base Haskell syntax wanted me to write *anything at all*, the time travel just worked! It's just that figuring out what the base Haskell syntax wanted out of me took all day.
@dysfun Yes, because megaparsec has readable documentation, and the simpler regex libraries don't.
@mcc i tried to learn it from Monadius game source code...
@mcc yeah, it's just that sounds doable with the prelude in less than 30 lines of code.
@dysfun What if I want to enforce formatting (EG, whitespace allowed in some situations and prohibited in others)?
@mcc that's starting to sound like more work than a couple of splits
@mcc Did you use Haskell to make Mario do BLJs?
@dysfun Well, then I'll use megaparsec.
Say I want to count the decimal digits in a Haskell Integer (ie, the bignum type). With a regular int I'd cast to double then do ceil(log10(i)), but the number might be poorly representable as double.
I try to look around the Haskell standard library. I get lost. I scroll:
https://hackage.haskell.org/package/base
I find a https://hackage.haskell.org/package/base-4.21.0.0/docs/GHC-Integer-Logarithms.html which looks like it offers a pure-integer arbitrary log base, but it says it's "for back compat" and returns "int#" (?)
Where's the REAL list of integral operations?
It seems like if you're already using any kind of bignum type (which Integer is) you ought to be able to at *least* give integer-ceiling log 2 trivially, because you know how many of your bignum constituents you're using to represent the bignum. So even if there's no integer logarithm on Haskell int (64-bit), I'd expect there to be a cheap one on Haskell integer (arbitrary precision).
@mcc it's only a bignum if it overflows 64 bits.
sorry 😅
@dysfun Then it can at least cap to within 64 bits…!
@dysfun @mcc i didn't read the puzzle description but here's a low-dependency implementation using lists for backtracking and the Prelude string handling functions (lines
words
) and some syntax I can't live without https://gist.github.com/plt-amy/a20616ed900d0451fc529d7aa6a21977
@saikou Thanks.
I find a "GHC-bignum" and a "integer-gmp" https://hackage.haskell.org/package/integer-gmp-0.5.1.0/docs/GHC-Integer-Logarithms.html is the latter just an older version of the same thing?
@saikou Thank you very much. I haven't got the hang of navigating the doc sites yet.
@dysfun @mcc i considered using guard
instead of the list comprehension but the thought of having exactly one import appealed to me in a way that it probably should not have. if i were doing more golfing i would've used a view pattern instead of a let binding for target
and just inlined operands
so the string handling code could be just a pair o'lines
Part 2, which is usually supposed to be a gotcha¹, took about 6 lines of code.
The only hard part was navigating the Haskell documentation. Which was *very hard* because GHC publishes both an integer-gmp and a ghc-bignum package (which uses gmp) and you have to figure out which one is fake
¹ I suspect, but haven't tested, the true "gotcha" here is that The AOC Writer assumes you're using 64-bit ints, but the part 2 conditions require use of bignums for correct answers.

To wrap up: Using Haskell was a powerful argument in favor of *a Haskell-like language* (the monadic style) and I kinda never want to write Haskell again. I found the documentation for both the base language and every library impenetrable; the tools to be clearly powerful at core but full of jagged edges in practice; the syntax got in my way more than it helped; and the ubiquitous <*- type operators constantly inhibited understanding.
But I think I'll try Idris and Purescript later this summer.
@mcc i have heard idris is quite good 😀
@mcc There's also Elm, which is a Haskell-like browser-side thing. It's about the only way I've found that I can stand to write web front-end stuff.
I, in the aspect of Eris, shall now throw a golden ball of discord into the garden of Mastodon functional programmers.
1. If I'm auditioning pure functional languages, should I try PureScript or Elm? Why?
2. What's the difference between `<<` and `<*`?
@mcc 1. purescript. because it's actually like haskell. elm doesn't even have typeclasses :/
2. they're the same these days
@mcc the answer for (2) is history
@mcc I can't reply to the first one, but the answer for the second one is: they're equivalent, but exist separately for historical reasons; <*
/*>
work with the Applicative
functor typeclass, which is a superset of Monad
(which has <<
/>>
), but was introduced later in time.
So for monads, both are equivalent, but <*
/*>
is more generalized, as it can be used with things that aren't necessarily monads.
A bit confusing, yes.
@dysfun @darkling The thing that worried me about Elm is I heard that the standard library has special abilities the user code can't because the standard library is exempt from certain typing requirements, but this means the language is inherently biased toward webtech because the standard library has a builtin library for that but doesn't have a whole lot else.
@dysfun What is Purescript's performance like? Does it approach Haskell?
@mcc it's about as literal a translation to javascript as you can imagine.
IO is a 0-ary function. bind is thus sequencing a load of 0-ary function calls. so fast enough in general, but never going to be as fast as native js.
@amy @mcc @darkling us mere mortals cannot be trusted with FFI.
imagine if we let you use FFI. it's all fun and games until someone pokes their eye out.
you'd come crying to me, "evan i poked my eye out" and i'd have to laugh in your face because as much as i pretend otherwise i absolutely fucking detest users.
@mcc the main slowdown is of course you won't be using javascript arrays in idiomatic purescript (or elm for that matter). you *can* have mutable collections if you need them, of course.
@mcc @darkling i'm going to warn you now that you haven't thought this through properly, but *shrug* have at it https://github.com/purerl/purerl
@mcc this is coming from someone who has only read a lot of the PureScript docs but has prototyped a couple things in Elm so I'm by no means authoritative
Elm is worth checking out but I think using it for the first time in this context will just lead to pain. it really shines when building interactive things imo and is annoying to do crunchier algorithms in
@iamevn Thanks. Maybe I'll find some other test environment for Elm.
@mcc
I was quite fluent in haskell about 10 years ago. I even wrote a proof of concept web browser in it for fun. Bit nowadays I don't understand the documentation anymore. It's getting more and more complex, which is really unfortunate because I loved this language and it will always be a very special language for me
Gonna be honest these questions created remarkably little discord among the Mastodon functional programmers. Everyone is pretty much on the same page.
@mcc I wish hls would deprioritize GHC imports in suggestions
@voyd I wonder if they'd be receptive to a filed feature request. It would make a lot of sense.
@mcc I'll have a look tomorrow. I tried to make some changes in HLS at one point but got stuck pretty quickly.
@mcc I agree with your intuition. Haskell is very much sharp edges and "in development by too few people", but a nice foundation to build advanced stuff. Like, having an FFI through a cleaner subset of C compilation IR, native codegen without other compilers, minimalist IR (core) that still typechecks like the most user facing source code... it's documentation, polish, usability and more people in front of it that keep it from getting more people in front of it, maybe sometimes by choice. 😅
@mcc is elm still alive? I thought it bit the dust years ago.
@mu @takeoutweight One thing I really did notice… the error messages repeatedly failed to highlight "where the actual problem was", and it made me think about how *extremely good* the Rust error messages are. And I don't think that's solely because Rust has different type information, I think it's because the Rust devs view "the error could have been more helpful than it was" as a critical issue. I wonder if Haskell would be easier with a Rust-like level of attention paid to good GHC errors.
@mcc @takeoutweight A little bit of irony that Haskell spawned Elm, which was an inspiration to better error messages in Rust, but Haskell still did not catch up. There are initiatives, but iiuc the nature of GHC being easy to change by smaller research efforts made it also harder for good error reporting. I assume that with enough effort GHC error messages could become nicer, just there's more people using Rust and Haskell tends to draw researchy folks who are up for some bleeding edge.
@mcc why do you use Haskell?
@alexchareshneu I am writing a program in Haskell because I do not know Haskell. I think it's worth knowing things just to know them.
@mcc every time I start reading about category theory I get a paragraph deep and my eyes glaze over and I can't. keep. going.
is there a baseline proficiency one can get to with Haskell (or another functional language?) where the category theory stops making your eyes glaze and you experience a spark of recognition?
or do you have to buckle down and do the reading first in order to "get it"
@scottcheloha I do not know category theory and I am not having problems with Haskell. I think the thing that would help more to learn is "point free programming". Or just say fuck Haskell and go learn an ML (like Rust…)
Incidentally, this is the video that made category theory make sense to me in a way other things before had not. https://web.archive.org/web/20250107162216mp_/https://cohost.org/mcc/post/75444-this-video-is-the-on I still don't know what it's good for.
@dysfun @mcc IO binds are optimized into sequential JS statements so they are about as fast as native JS could be.
Also, more recent is the new (optional) backend that produces highly optimized code. It performs aggressive inlining and compile time evaluation to produce code that is quite fast - https://github.com/aristanetworks/purescript-backend-optimizer