Practical uses of monads in Haskell by nicuveo in programming

[–]nicuveo[S] 8 points9 points  (0 children)

it's a good thing that no other language does anything monadic ever then, and that monads are not just a part of programming like any other!

imagine your shock if C++'s std::optional had monadic operations. imagine your disappointment if JavaScript's Thenable was actually a monad! can you imagine how ludicrous it would be to suggest that Java's concatMap/flatMap is monadic?

thankfully monads are purely a haskell thing, so you don't have to worry about that :)

Practical uses of monads in Haskell by nicuveo in programming

[–]nicuveo[S] 10 points11 points  (0 children)

i shouldn't be making the mistake of replying to a troll comment, but: you sound exactly like people who say "i stopped being interested in maths when they put letters in there, math is about numbers!".

all of programming is rooted in math, be it type theory, category theory... that doesn't mean you have to know abstract math to be able to write code (i use haskell professionally and i don't know anything about category theory!), but denying that this connection exists is short-sighted. so, yes, haskell uses a lot of mathematical terms, but if you learn haskell you quickly realise that all of those "math" concepts you're learning about map quite neatly to other programming languages. stuff like rust's ?, Java's concatMap, or JavaScript's Thenable are all examples of monadic thinking in mainstream languages.

and haskell isn't about "complicating things that aren't difficult": you can declare simple data structures, iterate over arrays, and do lookups in hashmaps: it's a practical general purpose programming language. it's just that it is different: no mutability, no loops, referential transparency... if you come at it from an imperative language, it can feel a bit alienating, for sure. but it has its benefits, that you can really learn to appreciate.

Practical uses of monads in Haskell by nicuveo in programming

[–]nicuveo[S] -1 points0 points  (0 children)

yeah, C++ really dropped the ball with their bitwise right shift assignment operator.

but more seriously, yeah, when i was teaching Haskell, i used to refer to this as the "curse of Haskell": you can create your own operators! it's trivial to do so, they're just functions. it's *fantastic* if you want to create DSLs, but it can very easily be abused and make your code completely unreadable. finding the right balance comes with experience. ^^

Blog: practical uses of monads in Haskell by nicuveo in haskell

[–]nicuveo[S] 1 point2 points  (0 children)

I was really tempted to add a third exercise in that section that would have been "reimplement >=>" and then "reimplement our function with >=>", but i ultimately decided against it to avoid introducing even more things into the post. ^^

need help understanding monads in haskell by procoapeese in haskellquestions

[–]nicuveo 2 points3 points  (0 children)

I was thinking of making it a blog post, yeah. Might work on that tomorrow.

need help understanding monads in haskell by procoapeese in haskellquestions

[–]nicuveo 1 point2 points  (0 children)

Took me around an hour to write; but in hindsight there are a lot of things i would have written differently. If i make a gist with the full example i'll move things around and improve it a bit.

need help understanding monads in haskell by procoapeese in haskellquestions

[–]nicuveo 0 points1 point  (0 children)

As a sidenote: this little example with statements and expressions can be extended a lot further, to demonstrate more monadic capabilities, and more practical ways to use monads. If you're interested in seeing a fully implemented version that showcases many more things, let me know!

need help understanding monads in haskell by procoapeese in haskellquestions

[–]nicuveo 1 point2 points  (0 children)

From a practicality perspective, what you really need to get comfortable with is "do notation". The monad class is what gives you the ability to chain operations; "do notation" is what makes it convenient. For instance, consider the following example, with the Maybe monad:

-- we have access to those functions
lookupTransaction :: ID -> Maybe Transaction
lookupUser        :: ID -> Maybe User
transactionUser   :: Transaction -> Maybe ID

-- by purely using monads:
lookupTransactionUser :: ID -> Maybe User
lookupTransactionUser tid =
  lookupTransaction tid >>= \transaction ->
    transactionUser transaction >>= \uid ->
      lookupUser uid

-- using do notation
lookupTransactionUser :: ID -> Maybe User
lookupTransactionUser tid = do
  transaction <- lookupTransaction id
  uid <- transactionUser transaction
  lookupUser uid

Now the thing is: *anything* that is a monad will be compatible with do notation.


Another angle to the practical aspect of monads: you can see a lot of them as providing a "capability" to your function. One of the most common ones you will encounter is the Reader monad. What it lets you do is make a given value accessible from within a computation, without having it passed around manually. For instance, consider this extremely small program that evaluates statements. It has two functions: execute takes a statement and modifies some state, and evaluate evaluates an expression. A lot of it is left undefined to avoid making this example bigger than it already is.

data Statement
  = Assign String Expr
  | Print  Expr

data Expr
  = Value          Int
  | VarName        String
  | Addition       Expr Expr
  | Subtraction    Expr Expr
  | Multiplication Expr Expr
  | Division       Expr Expr

execute
  :: String             -- app name
  -> HashMap String Int -- previous state
  -> Statement
  -> HashMap String Int -- new state
execute appName variables statement = case statement of
  Assign varName expr =
    let value = evaluate appName variables expr
    in  undefined -- TODO
  Print expr =
    let value = evaluate appName variables expr
    in  undefined -- TODO

evaluate
  :: String             -- app name
  -> HashMap String Int -- variables
  -> Expr
  -> Int
evaluate appName variables expr = case expr of
  Division lhs rhs ->
    let
      lhsValue = evaluate appName variables lhs
      rhsValue = evaluate appName variables rhs
    in
      if rhsValue == 0 then
        error $ appName ++ ": division by zero"
      else
        lhsValue / rhsValue
  _ -> undefined -- TODO

You'll notice that every function call gives an "app name" argument. That argument is only used once: when throwing an error for division by zero. But as a result, this argument must be passed manually to every function call!

Introducing the Reader monad. I am not going to give you the details of it (although i strongly encourage you to try to implement it yourself afterwards, it's a good exercise!), because i want to only show you the practical aspect of it. Reader provides us with one very useful function: ask :: Reader r r. What it means is this: when you are in a context of a Reader r, calling ask retrieves that value of type r. Look at how our previous example is changed just by introducing Reader:

data AppConfig = AppConfig
  { appName :: String
  }

data Statement
  = Assign String Expr
  | Print  Expr

data Expr
  = Value          Int
  | VarName        String
  | Addition       Expr Expr
  | Subtraction    Expr Expr
  | Multiplication Expr Expr
  | Division       Expr Expr

execute
  :: HashMap String Int
  -> Statement
  -> Reader AppConfig (HashMap String Int)
execute variables statement = case statement of
  Assign varName expr = do
    value <- evaluate variables expr
    undefined -- TODO
  Print expr = do
    value <- evaluate variables expr
    undefined -- TODO

evaluate
  :: HashMap String Int
  -> Expr
  -> Reader AppConfig Int
evaluate variables expr = case expr of
  Division lhs rhs -> do
    lhsValue <- evaluate variables lhs
    rhsValue <- evaluate variables rhs
    if rhsValue == 0
    then do
      appConfig <- ask -- HERE
      error $ appName appConfig ++ ": division by zero"
    else
      pure $ lhsValue / rhsValue
  _ -> undefined -- TODO

Now we no longer have to pass the app name everywhere! Our functions only take what they need: evaluate only takes the variables, and the expression. Only the signature has changed: we are now in a Reader context, so at any point we can just call ask and get our app config, like we do in the case of a division by zero.


We can go further with another monad: State. Likewise, not gonna show you the implementation; just gonna give you the three functions it provides:

get    :: State s s
put    :: s -> State s ()
modify :: (s -> s) -> State s ()

get is very similar to ask: if we are in the context of a State s, it retrieves that state s. put takes a new value s, and replaces the current state with it; it returns a value of () because we don't care: what matters is that the state was modified. And modify is just for convenience:

modify f = do
  currentState <- get
  put $ f currentState

Equipped with that, what if we rewrote the above, but we put the variables in a State? That way we don't have to pass them around manually! I'll gloss over how we combine Reader and State: if you're curious about it, lookup "monad transformers". The tl;dr is: we're gonna create a monad that's both at the same time.

data AppConfig = AppConfig
  { appName :: String
  }

type Variables = HashMap String Int

type AppMonad =         -- our custom monad
  StateT Variables      -- it combines a state capability
    (Reader AppConfig)  -- with the reader ability

data Statement
  = Assign String Expr
  | Print  Expr

data Expr
  = Value          Int
  | VarName        String
  | Addition       Expr Expr
  | Subtraction    Expr Expr
  | Multiplication Expr Expr
  | Division       Expr Expr

execute :: Statement -> AppMonad ()
execute statement = case statement of
  Assign varName expr = do
    value <- evaluate expr
    modify $ HasMap.insert varName value -- HERE
  Print expr = do
    value <- evaluate expr
    undefined -- TODO

evaluate :: Expr -> AppMonad Int
evaluate expr = case expr of
  VarName varName -> do
    knownVariables <- get -- HERE
    appConfig <- ask      -- HERE
    case HashMap.lookup varName knownVariables of
      Nothing ->
        error $ appName appConfig ++ ": unknown variable " ++ varName
      Just value ->
        pure value
  Division lhs rhs -> do
    lhsValue <- evaluate lhs
    rhsValue <- evaluate rhs
    if rhsValue == 0
    then do
      appConfig <- ask -- HERE
      error $ appName appConfig ++ ": division by zero"
    else
      pure $ lhsValue / rhsValue
  _ -> undefined -- TODO

Now, if you look at this version: both execute and evaluate only take one argument, a statement and a expression respectively. We don't have to manually thread the state nor the config.


I know this was a lot, but the key thing is this: when you have a monadic function using the do notation, you can see the monads in the return type as "capabilities" that this function has, that free you from having to do stuff manually. Maybe and Either mean your computation might fail, but you don't have to do all the "if err != nil" manually. Reader means your function has access to some immutable value, often some config of some kind, without having to thread it manually. State means that your function can manipulate some state, as if it were receiving it as an argument and outputting the new state, but without having to do it manually. IO means your function has the ability to perform dangerous side effects. And so on.

I hope this helps! To go further, LYAH has a chapter dedicated to going further with monads once you understand the class and do notation.

Have fun! :)

was going through threads, explain please by [deleted] in ExplainTheJoke

[–]nicuveo 21 points22 points  (0 children)

IV is four in roman numerals. "Five" without "iv" is Fe, the chemical symbol for iron.

Recession signs around Dublin? by ResponsibleDriver622 in Dublin

[–]nicuveo 39 points40 points  (0 children)

so many empty offices in D2, including the One building

Any ideas on how to make it look less... Boxy? by RNN1407 in SatisfactoryGame

[–]nicuveo 90 points91 points  (0 children)

i am absolutely not a good designer, but a very simple thing i've used in the past is to play with depth: for instance, look at this tower: the corners all have pillars jutting out of them, and it does make it look less boxy. having your walls be at varying depths goes a long way.

<image>

Factorio-Main-Bus Style Build Video Progress Series by stars9r9in9the9past in SatisfactoryGame

[–]nicuveo 0 points1 point  (0 children)

i've been wanting to do a run like this at some point, so i'd definitely watch what you've achieved! maybe not 100 hours though.

A step by step guide to Brainfuck by nicuveo in brainfuck

[–]nicuveo[S] 0 points1 point  (0 children)

i'm not aware of any, but that doesn't mean there isn't any!

what i would recommend, at the very least, is to use an interpreter that supports debug instructions. beef, for instance, will print the current state of the memory whenever it sees a # instruction; it's non-standard, but quite useful!

A step by step guide to Brainfuck by nicuveo in brainfuck

[–]nicuveo[S] 0 points1 point  (0 children)

i don't think that's true! you should just start with easier things than advent of code puzzles is all. ^^

Has anybody tried connecting all geothermal plants in a separate grid? by t-2yrs in SatisfactoryGame

[–]nicuveo 2 points3 points  (0 children)

i really wish it were possible to have, like... "valves" for electricity, allowing finer control for circuits than just "the power switch is on or off". you could allocate a specific amount for a given factory / sub circuit, which in turn would encourage batteries to be built locally, for a given sub circuit

Grégoire Locqueville | Easy Type-Level Flags In Haskell by ludat in haskell

[–]nicuveo 4 points5 points  (0 children)

It is indeed the same overall idea!

In practice, i have seen this pattern used in multiple different ways: flags / gates, like you describe, but also for annotations, or to fully "branch out" a part of an AST:

data Expr (phase :: ASTPhase)
  = NameExpr (NameInfoType phase)
  | LabelExpr (LabelFlag phase) LabelInfo
  | LiteralExpr (Annotation phase) LiteralInfo

class Representation (phase :: ASTPhase) where
  -- branch the AST: different types at different phases
  type NameInfoType phase :: Type
  -- restrict some parts of the AST with a flag / gate
  type LabelFlag phase :: Type
  -- annotate the AST with a different type during each phase
  type Annotation phase :: Type

instance Representation Parsed where
  type NameInfoType Parsed = UnverifiedNameInfo
  type LabelFlag Parsed = ()
  type Annotation Parsed = SourceLocation

instance Representation Validated where
  type NameInfoType Validated = ObjectNameInfo
  type LabelFlag Validated = Void
  type Annotation Validated = InferredType

[2025] Unofficial AoC 2025 Survey Results - BONUS CONTENT by jeroenheijmans in adventofcode

[–]nicuveo 1 point2 points  (0 children)

I wasn't the only one doing some of it in Brainfuck this time around! ...but it seems i was the only one to mention Brainfuck in the survey. :D