Railway Pattern
Question 1
To implement these classes and methods, just “convert” the Haskell definitions to Python code. Note that Validation
is not a monad.
from typing import Anyfrom dataclasses import dataclass
class List: @staticmethod def pure(x): return Node(x, Empty())
# Convenience method for Question 3 @staticmethod def from_list(ls): match ls: case []: return Empty() case x, *xs: return Node(x, List.from_list(xs))
@dataclassclass Node(List): head: object tail: List
def map(self, f): return Node(f(self.head), self.tail.map(f))
def ap(self, x): tails = self.tail.ap(x) heads = Node._ap(self.head, x) return heads.concat(tails)
# helper method @staticmethod def _ap(f, xs): match xs: case Empty(): return Empty() case Node(l, r): return Node(f(l), Node._ap(f, r))
def concat(self, xs): return Node(self.head, self.tail.concat(xs))
def flatMap(self, f): return f(self.head).concat(self.tail.flatMap(f))
@dataclassclass Empty(List): def map(self, f): return self def concat(self, xs): return xs def ap(self, x): return self def flatMap(self, f): return self
class Maybe: @staticmethod def pure(x): return Just(x)
@dataclassclass Just(Maybe): val: object
def map(self, f): return Just(f(self.val))
def ap(self, x): match x: case Just(y): return Just(self.val(y)) case Nothing(): return x
def flatMap(self, f): return f(self.val)
@dataclassclass Nothing: def map(self, f): return self def ap(self, x): return self def flatMap(self, f): return self
class Either: @staticmethod def pure(x): return Right(x)
@dataclassclass Left(Either): inl: object def map(self, f): return self def ap(self, f): return self def flatMap(self, f): return self
@dataclassclass Right(Either): inr: object
def map(self, f): return Right(f(self.inr))
def ap(self, f): match f: case Left(e): return f case Right(x): return Right(self.inr(x))
def flatMap(self, f): return f(self.inr)
class Validation: @staticmethod def pure(x): return Success(x)
@dataclassclass Success: val: object
def map(self, f): return Success(f(self.val))
def ap(self, f): match f: case Failure(e): return f case Success(x): return Success(self.val(x))
@dataclassclass Failure: err: list[str]
def map(self, f): return self
def ap(self, f): match f: case Failure(err): return Failure(self.err + err) case Success(x): return self
Question 2
Question 2.1: Unsafe Sum
The Python implementation of sum_digits
can be a Haskell rewrite of your sumDigits
solution for Question 6 in Chapter 1.4 (Course Introduction#Exercises):
def sum_digits(n): return n if n < 10 else \ n % 10 + sum_digits(n // 10)
Question 2.2: Safe Sum
The idea is to have sum_digits
return a Maybe
object. In particular, the function should return Nothing
if n
is negative, and Just x
when n
is positive and produces result x
.
def sum_digits(n): return Nothing() if n < 0 else \ Just(n) if n < 10 else \ sum_digits(n // 10).map(lambda x: x + n % 10)
sumDigits :: Int -> Maybe IntsumDigits n | n < 0 = Nothing | n < 10 = Just n | otherwise = (n `mod` 10 +) <$> sumDigits (n `div` 10)
Question 2.3: Final Sum
The result of sum_digits
is a Maybe[int]
, and sum_digits
itself has type int -> Maybe[int]
. To compose sum_digits
with itself we can use flatMap
or >>=
.
def final_sum(n): n = sum_digits(n) return n.flatMap(lambda n2: n2 if n2 < 10 else final_sum(n2))
finalSum :: Int -> Maybe IntfinalSum n = do n' <- sumDigits n if n' < 10 then Just n' else finalSum n'
Question 3
Question 3.1: Splitting Strings
split
in Python can be implemented with the str.split
method. The split
function for Haskell is shown in Chapter 4.4 (Railway Pattern#Validation).
# Uses the convenience method from_list in the List classdef split(char, s): return List.from_list(s.split(char))
Question 3.2: CSV Parsing
Split the string over \n
, then split each string in that list over ,
using map
:
def csv(s): return split('\n', s) .map(lambda x: split(',', x))
csv :: String -> [[String]]csv s = split ',' <$> (split '\n' s)
Question 4
Question 4.1: Factorial
Should be boring at this point.
def factorial(n): return 1 if n <= 1 else \ n * factorial(n - 1)
factorial :: Int -> Intfactorial 0 = 1factorial n = n * factorial (n - 1)
Question 4.2: Safe Factorial
The idea is to return a Left
if n
is negative, Right
with the desired result otherwise. Typically, Right
is the happy path.
def factorial(n, name): if n < 0: return Left(name + ' cannot be negative!') if n <= 1: return Right(1) return factorial(n - 1, name).map(lambda x: x * n)
factorial :: Int -> String -> Either String Intfactorial n name | n < 0 = Left $ name ++ " cannot be negative!" | n <= 1 = Right 1 | otherwise = (n*) <$> factorial (n - 1) name
Question 4.3: Safe n choose k
Idea: Compute ap
:
def choose(n, k): nf = factorial(n, 'n') kf = factorial(k, 'k') nmkf = factorial(n - k, 'n - k') div = lambda x: lambda y: lambda z: x // y // z return nf.map(div).ap(kf).ap(nmkf)
choose :: Int -> Int -> Either String Intchoose n k = let nf = factorial n "n" kf = factorial k "k" nmkf = factorial (n - k) "n - k" f x y z = x `div` y `div` z in f <$> nf <*> kf <*> nmkf
With the ApplicativeDo
language extension enabled, you can just use do
notation:
{-# LANGUAGE ApplicativeDo #-}choose :: Int -> Int -> Either String Intchoose n k = do nf <- factorial n "n" kf <- factorial k "k" nmkf <- factorial (n - k) "n - k" return $ nf `div` kf `div` nmkf
Question 4.4
Redefine factorial
to use Validation
instead of Either
:
def factorial(n, name): if n < 0: return Failure([f'{name} cannot be negative!']) if n <= 1: return Success(1) else: return factorial(n - 1, name).map(lambda x: n * x)
factorial :: Int -> String -> Validation [String] Intfactorial n name | n < 0 = Failure [name ++ " cannot be negative!"] | n <= 1 = Success 1 | otherwise = (n*) <$> factorial (n - 1) name
Finally, update the type signature of choose
(we do not need to do so in Python).
choose :: Int -> Int -> Validation [String] Intchoose n k = do nf <- factorial n "n" kf <- factorial k "k" nmkf <- factorial (n - k) "n - k" return $ nf `div` kf `div` nmkf