Skip to content

Commit

Permalink
Complete Monad
Browse files Browse the repository at this point in the history
  • Loading branch information
Abhijit Sarkar committed Dec 28, 2024
1 parent 1405e89 commit 4238c33
Show file tree
Hide file tree
Showing 7 changed files with 219 additions and 20 deletions.
2 changes: 2 additions & 0 deletions fp-course-haskell.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ library
ExactlyOne
Functor
List
Monad
Optional
Validation
other-modules:
Expand All @@ -46,6 +47,7 @@ test-suite fp-course-test
ApplicativeSpec
FunctorSpec
ListSpec
MonadSpec
OptionalSpec
Property
SpecHook
Expand Down
142 changes: 142 additions & 0 deletions src/Monad.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
{-# LANGUAGE InstanceSigs #-}
{-# LANGUAGE RebindableSyntax #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE NoImplicitPrelude #-}

module Monad where

import Applicative (Applicative)
import qualified Applicative as A
import Core
import ExactlyOne (ExactlyOne (..))
import qualified ExactlyOne as EO
import qualified Functor as F
import List (List (..))
import qualified List as L
import Optional (Optional (..))
import qualified Optional as O
import qualified Prelude as P ((=<<))

-- | All instances of the `Monad` type-class must satisfy one law. This law
-- is not checked by the compiler. This law is given as:
--
-- * The law of associativity
-- `∀f g x. g =<< (f =<< x) ≅ ((g =<<) . f) =<< x`
class (Applicative f) => Monad f where
-- Pronounced, bind.
(=<<) :: (a -> f b) -> f a -> f b

infixr 1 =<<

-- | Binds a function on the ExactlyOne monad.
--
-- >>> (\x -> ExactlyOne(x+1)) =<< ExactlyOne 2
-- ExactlyOne 3
instance Monad ExactlyOne where
(=<<) :: (a -> ExactlyOne b) -> ExactlyOne a -> ExactlyOne b
(=<<) = EO.bindExactlyOne

-- | Binds a function on a List.
--
-- >>> (\n -> n :. n :. Nil) =<< (1 :. 2 :. 3 :. Nil)
-- [1,1,2,2,3,3]
instance Monad List where
(=<<) :: (a -> List b) -> List a -> List b
(=<<) = L.flatMap

-- | Binds a function on an Optional.
--
-- >>> (\n -> Full (n + n)) =<< Full 7
-- Full 14
instance Monad Optional where
(=<<) :: (a -> Optional b) -> Optional a -> Optional b
(=<<) = O.bindOptional

-- | Binds a function on the reader ((->) t).
--
-- >>> ((*) =<< (+10)) 7
-- 119
instance Monad ((->) t) where
(=<<) :: (a -> (->) t b) -> (->) t a -> (->) t b
f =<< g = \x -> f (g x) x

-- | Witness that all things with (=<<) and (<$>) also have (<*>).
--
-- >>> ExactlyOne (+10) <**> ExactlyOne 8
-- ExactlyOne 18
--
-- >>> (+1) :. (*2) :. Nil <**> 1 :. 2 :. 3 :. Nil
-- [2,3,4,2,4,6]
--
-- >>> Full (+8) <**> Full 7
-- Full 15
--
-- >>> Empty <**> Full 7
-- Empty
--
-- >>> Full (+8) <**> Empty
-- Empty
--
-- >>> ((+) <**> (+10)) 3
-- 16
--
-- >>> ((+) <**> (+5)) 3
-- 11
--
-- >>> ((+) <**> (+5)) 1
-- 7
--
-- >>> ((*) <**> (+10)) 3
-- 39
--
-- >>> ((*) <**> (+2)) 3
-- 15
(<**>) :: (Monad f) => f (a -> b) -> f a -> f b
(<**>) = (A.<*>)

infixl 4 <**>

-- | Flattens a combined structure to a single structure.
--
-- >>> join ((1 :. 2 :. 3 :. Nil) :. (1 :. 2 :. Nil) :. Nil)
-- [1,2,3,1,2]
--
-- >>> join (Full Empty)
-- Empty
--
-- >>> join (Full (Full 7))
-- Full 7
--
-- >>> join (+) 7
-- 14
join :: (Monad f) => f (f a) -> f a
join = (=<<) id

-- | Implement a flipped version of @(=<<)@, however, use only
-- @join@ and @(<$>)@.
-- Pronounced, bind flipped.
--
-- >>> ((+10) >>= (*)) 7
-- 119
(>>=) :: (Monad f) => f a -> (a -> f b) -> f b
a >>= f = join (f F.<$> a)

infixl 1 >>=

-- | Implement composition within the @Monad@ environment.
-- Pronounced, kleisli composition.
--
-- >>> ((\n -> n :. n :. Nil) <=< (\n -> n+1 :. n+2 :. Nil)) 1
-- [2,2,3,3]
(<=<) :: (Monad f) => (b -> f c) -> (a -> f b) -> a -> f c
f <=< g = \x -> f =<< g x

infixr 1 <=<

-----------------------
-- SUPPORT LIBRARIES --
-----------------------

instance Monad IO where
(=<<) =
(P.=<<)
21 changes: 10 additions & 11 deletions test/ApplicativeSpec.hs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
module ApplicativeSpec (spec) where

import Applicative ()
import qualified Applicative as A
import ExactlyOne (ExactlyOne (..))
import qualified Functor as F
Expand All @@ -16,13 +15,13 @@ spec = do
prop "pure == ExactlyOne" $
\x -> pure x `shouldBe` ExactlyOne (x :: Integer)
it "Applying within ExactlyOne" $
ExactlyOne (+ 10) <*> ExactlyOne 8 `shouldBe` ExactlyOne (18 :: Int)
ExactlyOne (+ 10) A.<*> ExactlyOne 8 `shouldBe` ExactlyOne (18 :: Int)

describe "List instance" $ do
prop "pure" $
\x -> pure x `shouldBe` (x :: Integer) :. Nil
it "<*>" $
(+ 1) :. (* 2) :. Nil <*> L.listh [1, 2, 3] `shouldBe` L.listh [2 :: Int, 3, 4, 2, 4, 6]
(+ 1) :. (* 2) :. Nil A.<*> L.listh [1, 2, 3] `shouldBe` L.listh [2 :: Int, 3, 4, 2, 4, 6]

describe "lift1" $ do
it "ExactlyOne" $
Expand All @@ -36,23 +35,23 @@ spec = do
prop "pure" $
\x -> pure x `shouldBe` Full (x :: Integer)
it "Full <*> Full" $
Full (+ 8) <*> Full 7 `shouldBe` Full (15 :: Int)
Full (+ 8) A.<*> Full 7 `shouldBe` Full (15 :: Int)
it "Empty <*> Full" $
Empty <*> Full "tilt" `shouldBe` (Empty :: Optional Integer)
Empty A.<*> Full "tilt" `shouldBe` (Empty :: Optional Integer)
it "Full <*> Empty" $
Full (+ 8) <*> Empty `shouldBe` (Empty :: Optional Int)
Full (+ 8) A.<*> Empty `shouldBe` (Empty :: Optional Int)

describe "Function instance" $ do
it "addition" $
((+) <*> (+ 10)) 3 `shouldBe` (16 :: Int)
((+) A.<*> (+ 10)) 3 `shouldBe` (16 :: Int)
it "more addition" $
((+) <*> (+ 5)) 3 `shouldBe` (11 :: Int)
((+) A.<*> (+ 5)) 3 `shouldBe` (11 :: Int)
it "even more addition" $
((+) <*> (+ 5)) 1 `shouldBe` (7 :: Int)
((+) A.<*> (+ 5)) 1 `shouldBe` (7 :: Int)
it "addition and multiplication" $
((*) <*> (+ 10)) 3 `shouldBe` (39 :: Int)
((*) A.<*> (+ 10)) 3 `shouldBe` (39 :: Int)
it "more addition and multiplcation" $
((*) <*> (+ 2)) 3 `shouldBe` (15 :: Int)
((*) A.<*> (+ 2)) 3 `shouldBe` (15 :: Int)
prop "pure" $
\x y -> pure x (y :: Integer) `shouldBe` (x :: Integer)

Expand Down
57 changes: 57 additions & 0 deletions test/MonadSpec.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
module MonadSpec (spec) where

import ExactlyOne (ExactlyOne (..))
import List (List (..))
import qualified Monad as M
import Optional (Optional (..))
import Test.Hspec

spec :: Spec
spec = do
describe "(=<<)" $ do
it "ExactlyOne" $
((\x -> ExactlyOne (x + 1)) M.=<< ExactlyOne 2) `shouldBe` ExactlyOne (3 :: Int)
it "List" $
((\n -> n :. n :. Nil) M.=<< (1 :. 2 :. 3 :. Nil)) `shouldBe` ((1 :: Int) :. 1 :. 2 :. 2 :. 3 :. 3 :. Nil)
it "Optional" $
((\n -> Full (n + n)) M.=<< Full 7) `shouldBe` Full (14 :: Int)
it "(->)" $
((*) M.=<< (+ 10)) 7 `shouldBe` (119 :: Int)

describe "<**>" $ do
it "ExactlyOne" $
ExactlyOne (+ 10) M.<**> ExactlyOne 8 `shouldBe` ExactlyOne (18 :: Int)
it "List" $
(+ 1) :. (* 2) :. Nil M.<**> 1 :. 2 :. 3 :. Nil `shouldBe` ((2 :: Int) :. 3 :. 4 :. 2 :. 4 :. 6 :. Nil)
it "Optional" $
Full (+ 8) M.<**> Full 7 `shouldBe` Full (15 :: Int)
it "Optional - empty function" $
Empty M.<**> Full (7 :: Int) `shouldBe` (Empty :: Optional Int)
it "Optional - empty value" $
Full (+ 8) M.<**> Empty `shouldBe` (Empty :: Optional Int)
it "(->) 1" $
((+) M.<**> (+ 10)) 3 `shouldBe` (16 :: Int)
it "(->) 2" $
((+) M.<**> (+ 5)) 3 `shouldBe` (11 :: Int)
it "(->) 3" $
((+) M.<**> (+ 5)) 1 `shouldBe` (7 :: Int)
it "(->) 4" $
((*) M.<**> (+ 10)) 3 `shouldBe` (39 :: Int)
it "(->) 5" $
((*) M.<**> (+ 2)) 3 `shouldBe` (15 :: Int)

describe "join" $ do
it "List" $
M.join ((1 :. 2 :. 3 :. Nil) :. (1 :. 2 :. Nil) :. Nil) `shouldBe` ((1 :: Int) :. 2 :. 3 :. 1 :. 2 :. Nil)
it "Optional with Empty" $
M.join (Full Empty) `shouldBe` (Empty :: Optional Integer)
it "Optional all Full" $
M.join (Full (Full 7)) `shouldBe` Full (7 :: Int)
it "(->)" $
M.join (+) 7 `shouldBe` (14 :: Int)

it "(>>=)" $
((+ 10) M.>>= (*)) 7 `shouldBe` (119 :: Int)

it "kleislyComposition" $
((\n -> n :. n :. Nil) M.<=< (\n -> n + 1 :. n + 2 :. Nil)) 1 `shouldBe` ((2 :: Int) :. 2 :. 3 :. 3 :. Nil)
10 changes: 5 additions & 5 deletions test/OptionalSpec.hs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module OptionalSpec (spec) where

import Optional (Optional (..), (<+>))
import Optional (Optional (..))
import qualified Optional as O
import Test.Hspec

Expand Down Expand Up @@ -30,13 +30,13 @@ spec = do

describe "<+>" $ do
it "first Full" $
Full 8 <+> Empty `shouldBe` Full (8 :: Int)
Full 8 O.<+> Empty `shouldBe` Full (8 :: Int)
it "both Full" $
Full 8 <+> Full 9 `shouldBe` Full (8 :: Int)
Full 8 O.<+> Full 9 `shouldBe` Full (8 :: Int)
it "first Empty" $
Empty <+> Full 9 `shouldBe` Full (9 :: Int)
Empty O.<+> Full 9 `shouldBe` Full (9 :: Int)
it "both empty" $
Empty <+> Empty `shouldBe` (Empty :: Optional Integer)
Empty O.<+> Empty `shouldBe` (Empty :: Optional Integer)

describe "optional" $ do
it "replaces full data constructor" $
Expand Down
5 changes: 3 additions & 2 deletions test/Property.hs
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@

module Property where

import List (List, listh)
import List (List)
import qualified List as L
import Test.QuickCheck
import Validation (Validation (..))

instance (Arbitrary a) => Arbitrary (List a) where
arbitrary = listh <$> arbitrary
arbitrary = L.listh <$> arbitrary

instance (Arbitrary a) => Arbitrary (Validation a) where
arbitrary =
Expand Down
2 changes: 0 additions & 2 deletions test/ValidationSpec.hs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@ import Test.Hspec.QuickCheck
import Validation (Validation (..))
import qualified Validation as V

-- f :: (Integral a) => a -> Validation a

spec :: Spec
spec = do
describe "isError" $ do
Expand Down

0 comments on commit 4238c33

Please sign in to comment.