pleroma.debian.social

pleroma.debian.social

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.

7.1 Standard I/O Functions

Although Haskell provides fairly sophisticated I/O facilities, as defined in the IO library, it is possible to write many Haskell programs using only the few simple functions that are exported from the Prelude, and which are described in this section.

All I/O functions defined here are character oriented. The treatment of the newline character will vary on different systems. For example, two characters of input, return and linefeed, may read as a single newline character. These functions cannot be used portably for binary I/O.

@mcc I *think* Char is actually (a) Unicode

@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.

@mcc

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?

$ ghc -O2 --make A.hs -threaded -rtsopts

@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.

Demonstration of ghc --make in gnome-terminal

@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 ?

https://cabal.readthedocs.io/en/stable/nix-local-build.html

@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~.

Run from command line:

@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

@mcc

Monad monoid endofuctors etc

@mcc yeah

@onelson @mcc bananurrito

@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.

@onelson @mcc (I hate these metaphors so much because if you take them to their logical conclusion you have to start thinking about things like "now you unwrap this banana and put these bananas inside and then wrap it all up into one new banana")

@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.

@megmac @mcc idk if this is anything but I have long held that monads are just like null propagation in sql, e.g. how `null or false` is `null` not `false`.

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]

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 I’m out of touch on current fashions, but my last project used stack and it solves a couple of the issues you’ve hit so far
replies
0
announces
0
likes
0

@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 openFile :: FilePath -> IOMode -> Bool -> IO (FD, IODeviceType)

@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!

@glyph @mcc we live in a mysterious universe.

@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?

-- 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
            
                   let result = 1
                   takeLine inHandle (acc + result) -- 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

                let result = 1
                takeLine inHandle (acc + result) -- 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

                    let result = 1
                    takeLine inHandle (acc + result)

@dysfun @mcc Rust actually has a method called literally and_then on both Option and Result, so one can just imagine a trait that has and_then and that's what Monad is (and it's not actually expressible in Rust right now heh).

It's the IO type itself that's the "wild" thing :)

@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, []) The documentation says space matches a space and space1 matches zero or more spaces. 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

@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?

@mcc

see also skipSome and spaceChar

sounds like a recipe for making space1.

@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 @dysfun hmm, do you run your program through cabal command? if not, it might be IDE doesn't see this cabal file and uses default vals

@froufox @dysfun `cabal run` gets the same error

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) 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)

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.

@mcc @dysfun the correct way is to find a command showing you the actual versions of the dependencies. not sure, but maybe something like `cabal list --installed`

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.

spaceThenNumber :: LineParser Int
spaceThenNumber = do
    let spaces = L.skipSome spaceChar
    _ <- spaces -- This works
    L.decimal spaceThenNumber :: LineParser Int
spaceThenNumber = do
    let spaces = L.skipSome spaceChar
    --_ <- spaces -- Commenting this line out breaks it
    L.decimal 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
   |                                         ^^^

@froufox @mcc we just tried that one. i'm sure it used to list versions...

@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 @dysfun `cabal freeze` should make a lockfile for you called "cabal.project.freeze".

@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 @dysfun Also, I think you got the weird output because you ran `--list installed`, not `list --installed`.

@samir @dysfun This worked much better

@samir @dysfun This is great. If this file is present but dist-newstyle is not, will cabal build automatically consult it? Do you recommend I commit this file to version control?

@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

@dysfun @mcc there must be something else showing a dependency tree for a given project...

@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 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
   |                                         ^^^^^^^^^^^^^^^

@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

runParser
  :: Parsec e s a -- ^ Parser to run
  -> String     -- ^ Name of source file
  -> s          -- ^ Input for parser
  -> Either (ParseErrorBundle s e) a ----------------------------------------------------------------------------
-- Running a parser

-- | @'parse' p file input@ runs parser @p@ over 'Identity' (see
-- 'runParserT' if you're using the 'ParsecT' monad transformer; 'parse'
-- itself is just a synonym for 'runParser'). It returns either a
-- 'ParseErrorBundle' ('Left') or a value of type @a@ ('Right').
-- 'errorBundlePretty' can be used to turn 'ParseErrorBundle' into the
-- string representation of the error message. See "Text.Megaparsec.Error"
-- if you need to do more advanced error analysis.
--
-- > main = case parse numbers "" "11,2,43" of
-- >          Left bundle -> putStr (errorBundlePretty bundle)
-- >          Right xs -> print (sum xs)
-- >
-- > numbers = decimal `sepBy` char ','
parse ::
  -- | Parser to run
  Parsec e s a ->
  -- | Name of source file
  String ->
  -- | Input for parser
  s ->
  Either (ParseErrorBundle s e) a
parse = runParser

@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:

-- 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) <- parse "DUMMY-FILENAME" parseLine inStr
                    takeLine inHandle (acc + lsum) 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 -- 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" app/Puzzle.hs:45:9: error: [GHC-58481]
    parse error (possibly incorrect indentation or mismatched brackets)
   |
45 |         _ -> error "Expected exactly one argument"
   |         ^

@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:

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.

@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 @Norgg
die unless [extremely trivial computery thing];

@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

@adriano @Norgg

fix(YOUR, $hearts) or die;

@mcc is this just summing numbers from a file?
putStrLn . 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 @Norgg bless;

@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

@mcc @dneto I am not Haskell fan but the whole monad thing is simply a way for Haskell to interact with the real world (aka not pure computing but IO/networking). It is a designed language that reallu try to force you into writing asynchronous code.

@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.

@mark @mcc my supervisor has a habit of writing out “alpha”, “beta” etc for type variables. I struggled a lot with that

@mcc @dysfun I *think* so but I feel like there’s some caveats that I’m forgetting. Sorry. If they come to me, I’ll let you know.

Yes, I recommend committing it unless you’re building some kind of general-purpose library.

@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.

@mcc

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.

@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

@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?

same screenshot but -1 is negate 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))

@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))

(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.

@mcc @cford

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"); }

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

@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`

@clementd Okay. But I did try that and it wouldn't let me use case…of either.

-- Read one value from command line, feed it to takeLines, print result
main :: IO ()
main = do
    case getArgs of
        [inFile] ->
            file <- openFile inFile ReadMode
            total <- takeLines file            -- TODO do in loop
            putStr (show total)
            putStr "\n"
        _ ->
            putStr "Bad Arguments"

app/Puzzle.hs:55:5: error: [GHC-53786]
    (case ... of ...)-syntax in pattern
   |
55 |     case getArgs 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
   |         ^^^^^^^^

@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:

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

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".

That's the right answer! You are one gold star closer.

@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

@amy @mcc bless you and your strictness annotations. most people don't bother.

@amy @dysfun Hm.

This is sincerely interesting to me, but also, given that the Megaparsec solution was 30 lines and this is 20 lines, I'm not sure this is a win.

@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?

@mcc @amy i would have done it in this style as well. probably not as well if i'm honest.

@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

@amy @mcc meanwhile i'm slightly scared by LambdaCase. it's not natural!

Part 2, which is usually supposed to be a gotcha¹, took about 6 lines of code.

https://github.com/mcclure/aoc2024/blob/a694551565afa1bb92bdaf9615af3ddf19404ccf/07-02-operations/app/Puzzle.hs

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.

That's the right answer! You are now one gold star closer.

You have completed day 7.

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.

@mcc @amy so between monads and today, we discovered applicatives, which gave us the language of *> and <* (and `pure` instead of `return`)

@mcc @dysfun one has a Monad constraint and the other has an Applicative constraint. which since 2014 every Monad is necessarily an Applicative you can always use <* and it results in more general code but it used to be the case that << and <* were incomparably general

@dysfun What is Purescript's performance like? Does it approach Haskell?

@mcc @darkling typing requirements? oh no, it's as simple as you cannot be trusted with the nice things. i would never write elm because of this.

purescript on the other hand is pretty alright for FFI

@mcc @dysfun As I understand it, it's *very* much oriented towards running inside a browser. I don't think I'd even think of using it in another context.

@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.

@mcc @dysfun @darkling the biggest turn-off about Elm is the paternalism. all the error messages talk to you like you're baby and the compiler is feeling really bad and Evan is the only person who can do FFI and etc

@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.

@darkling @mcc some people use it for js. there are even some nutters who use it to generate erlang.

@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.

@dysfun @darkling *tilts head* I'm interested in generating erlang

@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

@dysfun @amy @mcc @darkling where are my eyes memory mapped, anyway?...

@flippac @amy @mcc @darkling the pineal gland, where all the DMT you need to pretend elm is good is released when you dream.

@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.

@dysfun
If you're interested in purescript and erlang, you might be interested in the gleam programming language
@mcc @darkling

@jaj @mcc @darkling you mean aside from the fact that the type system is nothing like purescript?

@jaj @mcc @darkling (i knew the author before they were mildly famous)

@jaj @dysfun @darkling It's on my list! However I don't think I'll actually reach Erlang, Elixir, Gleam as part of this AOC project because the fact is AOC problem are all about batch processing and I'm not sure that's what the Erlang-VM family actually excels at.

@dysfun
I'm not familiar enough to compare the type systems of both languages but at least it is typed
@mcc @darkling

@mcc @dysfun Me too. Although running erlang in the browser would be better. :)

BEAM on WASM?

@darkling @mcc i was going to say that's been done, but it looks like the project is dead now.

@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 @amy From my own domain (cryptography): A lot! GHC is very efficient in generating good Assembly from primitive operations (those with '#' at the end, though this is only convention), and libgmp or the other bignum implementations you can use from GHC Haskell are orders of magnitude slower.

@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.

@mcc @zmz my fortunate or unfortunate choice of username has somehow landed me in a Haskell community discussion on regular expressions.

Wrong number. Figures.

@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

@haskman @mcc what the fuck, arista uses purescript?!

@dysfun @mcc And quite successfully so! The frontend for our network security product is written in PureScript (migrated over from JavaScript)

@haskman @mcc i had no idea. don't suppose you're looking for more people with purescript experience?

@dysfun @mcc unfortunately we have no open positions on the team that I am aware of

@haskman @mcc ah well. still cool to hear you're using purescript.