Updated

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

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

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

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

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

@dataclass
class Left(Either):
    inl: object
    def map(self, f): return self
    def ap(self, f): return self
    def flatMap(self, f): return self

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

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


@dataclass
class 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 Int
sumDigits 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 Int
finalSum 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 class
def 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 -> Int
factorial 0 = 1
factorial 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 Int
factorial 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 \(n!\), \(k!\) and \((n - k)!\) in "parallel", combine with 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 Int
choose 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 Int
choose 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] Int
factorial 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] Int
choose n k = do
  nf <- factorial n "n"
  kf <- factorial k "k"
  nmkf <- factorial (n - k) "n - k"
  return $ nf `div` kf `div` nmkf