It shows how algorithms with mutable state can be implemented with pure functions, with immutable variables only.
The series itself is an excuse to take several detours on other Functional Programming topics: currying, partial application, recursion, Functors, Applicative Functors.
The source code includes F# and C# examples. In the next weeks it will be followed by code examples showing how to apply a state monad in practice.
On first glance, your example:
(decimal | DivideByZeroException) Divide(decimal n, decimal d) => n / d;
Isn't that far off from what is pretty easily done in C#: using System.Runtime.CompilerServices;
(decimal? , DivideByZeroException?) Divide(decimal n, decimal d) {
try {
return (n / d, null);
} catch {
return (null, new DivideByZeroException());
}
}
Divide(6, 2).Switch(
(result) => Console.WriteLine($"6 /2 is {result}"),
(ex) => Console.WriteLine("This is an exception!")
);
Divide(6, 0).Switch(
(result) => Console.WriteLine($"6 /2 is {result}"),
(ex) => Console.WriteLine("This is an exception!")
);
public class DivideByZeroException : Exception;
public static class TupleExtension {
public static void Switch<T, E>(this ValueTuple<T, E> tuple, Action<T> action1, Action<E> action2) {
if (tuple.Item1 is null && tuple.Item2 is null) {
} else if (tuple.Item1 is null) {
action2(tuple.Item2);
} else {
action1(tuple.Item1);
}
}
}
(Sharing for folks who aren't familiar with C# Tuple types and extension methods) (null, null)
(result, new DivideByZeroException())
Hence your unhandled base case in the extension method. Sum types are a more accurate representation and more pleasant to use as a result.(btw, you're looking through the Monads For The Rest Of Us tutorial, but the the State Monad For The Rest Of Us tutorial is the one linked to this thread)
One minor thought: where you have `A Tree is either a Leaf or a Node containing 2 branches each containing another Tree structure.`, perhaps you could have `A Tree is either a Leaf or a Node _made of_ 2 branches each containing another Tree structure.`
So that "made of" easily maps to the "of" in F#'s type definition. Just an idea.
Haskell will bring you more of the benefits from FP with expressive types; Idris will bring you even more. But you will lose the integration.
Same as F#, OCaml, etc.
I'm primarily a C# dev, but after dabbling in some F#, I concluded that most of the niceties in F# could also be done (to some extent and with some limitations) on C# as they have the same compile targets.
- representation of "the real world" where you can send data, control things, etc;
- representation of early return error semantics, like you have with exceptions;
- encapsulation of different execution threads, so you only interface with the group;
- encapsulation of a computer architecture, so something compiled for your GPU runs on the GPU, and something compiled for your CPU run on your CPU;
- encapsulation of transactions, so you can have a multi-threaded software changing whatever shared value you want and none of that leaks to the larger software;
- data structure, like libraries that encode hardware description into them.
That list is not in any way comprehensive. It's just stuff that I can remember right now.
For example, this is a really minimalistic arithmetic syntax tree interpreter in Haskell. You could write errors using the Either type:
data Term = Constant Int | Divide Term Term
eval :: Term -> Either String Int
eval (Constant n) = n
eval (Divide left right) =
case eval left of
Left errorString -> Left errorString
Right leftResult ->
case eval right of
Left errorString -> Left errorString
Right rightResult ->
if rightResult == 0
then Left "Division by zero"
else Right (div leftResult rightResult)
It's cumbersome, and there's a staircase growing. Because Either is a monad, you could rewrite the division op like this: eval (Divide left right) = do
leftResult <- eval left
rightResult <- eval right
if rightResult == 0
then Left "Division by zero"
else Right (div leftResult rightResult)
This is a good article about the either monad: https://mmhaskell.com/blog/2022/3/3/using-either-as-a-monadThe state monad is basically a wrapper over a pure function that takes a state and returns a tuple with the result and a new state. In an imperative language, you'd just actually change state, rather than doing that.
An example from this wiki page: https://en.wikibooks.org/wiki/Haskell/Understanding_monads/S...
In an imperative language, for pseudo-random numbers, you could modify global variables every time you want a new random number. Haskell lets you do this in IO functions. But a pure way would be to return the result of the new global variable every time.
-- randomR takes a range, so randomR (1,6) produces a random number between 1 and 6
rollPair :: StdGen -> ((Int, Int), StdGen)
rollPair s0 =
let (r1, s1) = randomR (1,6) s0
(r2, s2) = randomR (1,6) s1
in ((r1, r2), s2)
It's annoying. You could use the state monad to simplify this: rollDieS :: State StdGen Int
rollDieS = state (randomR (1,6))
rollPairS :: State StdGen (Int, Int)
rollPairS = do
r1 <- rollDieS
r2 <- rollDieS
return (r1, r2) eval (Constant n) = Right n
eval (Constant n) = return nSo... it's a monad? I don't understand why this is worth calling State as distinct from non-State when that doesn't seem to add any meaning. Surely there must be something implied by a State monad that is not implied by a non-State monad. That seems relevant to call out in an introduction.
The key idea is that you can have something ressembling states with pure functions if you pass the a representation of the initial state as an argument and have the modified state be part of what the function return. You can then use this modified state as an argument for the next function call. The State monad is just an handy way to do this chaining.
But, yes, the actual state representation is in the way the argument is encoded, that's true.
Edit: I find it hilarious that I’m the one downvoted here while all the comments saying complete non sense about monads - including the ones clearly having no clue of what a monad actually is - are not. Never change HN.
A week or two ago, I found myself working in the same codebase. I had to do some refactoring and, thanks to the monad I had already established, the refactoring reduced code while expanding functionality. The best refactors happen when the patterns make it easy to do.
St -> (a, St)
can viewed as `State St' parameterized over `a'. There are several behaviors that organize such functions, such as Monad which "overloads the semicolon" to work on stateful computations. Another behavior implements a stateful interface: `MonadState St'. get :: St -> (St, St)
put :: St -> (St -> ((), St))
With type classes in Haskell, behaviour is type-directed so in order to reap the benefits we declare a new type. {-# Language DerivingVia #-}
{-# Language GADTSyntax #-}
-- get :: My St
-- put :: St -> My ()
newtype My a where
My :: (St -> (a, St)) -> My a
deriving (Functor, Applicative, Monad, MonadState St, MonadFix)
via State St St -> (a, St)
vs St -> (St, a)
I've found both in the wild and it can be very confusing!A monad is just a type constructor and two functions respecting some rules.
It can be widely different from the State monad ranging from useful (Option) to completely weird (the naive list monad) to doing nothing (identity monad).