- replies
- 0
- announces
- 0
- likes
- 0
@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
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.
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.
@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".

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...
@dysfun What if I want to enforce formatting (EG, whitespace allowed in some situations and prohibited in others)?
@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).
@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 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 the answer for (2) is history
@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 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