Skip to content

Instantly share code, notes, and snippets.

@jolod
Last active October 1, 2017 17:50
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jolod/744f143a954985bf3e4159d14146d3cd to your computer and use it in GitHub Desktop.
Save jolod/744f143a954985bf3e4159d14146d3cd to your computer and use it in GitHub Desktop.
Rank-1 vs Rank-2 vs Applicative
newtype Point a = Point {x :: a, y :: a, z :: a}
-- Let's say you start out by wanting to write this getxPair function.
getxPair :: forall a b. Point a -> Point b -> Tuple a b
getxPair p1 p2 = Tuple ((\(Point {x}) -> x) p1) ((\(Point {x}) -> x) p2)
-- And then you write a getyPair function. But the repeated function \(Point {x}) -> x)
-- doesn't look so nice, so this time you name it in a where clause.
getyPair :: forall a. Point a -> Point a -> Tuple a a
getyPair p1 p2 = Tuple (gety p1) (gety p2)
where
gety (Point {y}) = y
-- Interestingly, the inferred types are different. The previous one is more general!
-- Maybe you decide that the get functions are useful enough to be at the top level.
getz :: forall a. Point a -> a
getz (Point {z}) = z
getzPair :: forall a b. Point a -> Point b -> Tuple a b
getzPair p1 p2 = Tuple (getz p1) (getz p2)
-- Now you got the general type back again! What's going on?
-- Now that you have the top-level getters, you don't want to repeat all the getPair
-- functions, so you take the getter as an argument.
getPair :: forall a b. (a -> b) -> a -> a -> Tuple b b
getPair get p1 p2 = Tuple (get p1) (get p2)
-- This function does not depent on Point at all, but the two arguments must be the
-- same. What if you have points of different of types? You'd want the return value
-- to be Tuple a b, not Tuple b b.
-- It seem like we need to either write specialized functions for each property,
-- or require that the type parameter in Point is the same. But there is a solution.
-- The problem in getyPair is that the type for gety is `Point a -> a`, like
getyPair' :: forall a. Point a -> Point a -> Tuple a a
getyPair' p1 p2 = Tuple (gety p1) (gety p2)
where
gety :: Point a -> a
gety (Point {y}) = y
-- The a in gety, is the same as in getyPair'. But we know that gety is more general,
-- and if we just take the type from the top-level definition of getz, we would write
getyPair'' :: forall a b. Point a -> Point b -> Tuple a b
getyPair'' p1 p2 = Tuple (gety p1) (gety p2)
where
gety :: forall c. Point c -> c -- Note the "forall".
gety (Point {y}) = y
-- where I renamed the type variable in gety to c to avoid confusion. The inferred type
-- is now as general as getxPair, returning Tuple a b. If we now change gety to be an
-- argument, and explicitly paste in the type, we get
getyPair''' :: forall a b. (forall c. Point c -> c) -> Point a -> Point b -> Tuple a b
getyPair''' gety p1 p2 = Tuple (gety p1) (gety p2)
-- This is exactly the same definition as getPair, but the return type is more general.
-- In fact, we no longer need depend on Point, so that can be quantified over as well.
-- See Rank2.purs.
-- Also, see Applicative.purs for a completely different approach.
-- Maybe it's worth commenting on why the type of getPair cannot be
-- forall a b c. (Point c -> c) -> Point a -> Point b -> Tuple a b
-- i.e. with the forall moved out of the parenthesis. That would mean that
-- there is ONE set of types, (a, b, c), that matches the implementaiton.
-- But how do you choose c so that you can do both (Point a -> a) AND (Point b -> b)?
-- You can't. That's why you need to say that the function can take all types, including
-- a and b, and this you do through the nested forall.
-- Paste into http://try.purescript.org.
module Main where
import Prelude
import Control.Monad.Eff (Eff)
import Data.Foldable (fold, foldMap)
import TryPureScript (DOM, h1, h2, p, text, list, indent, link, render, code)
import Data.Tuple (Tuple(..))
newtype Point a = Point {x :: a, y :: a}
getx (Point {x}) = x
gety (Point {y}) = y
getPair :: forall t a b. (forall c. t c -> c) -> t a -> t b -> Tuple a b
getPair get r1 r2 = Tuple (get r1) (get r2)
pointValues = Point {x: 3, y: 4}
pointLabels = Point {x: "x", y: "y"}
xPair = getPair getx pointValues pointLabels
yPair = getPair gety pointValues pointLabels
main :: Eff (dom :: DOM) Unit
main =
render $ foldMap (\s -> p (text s)) output
where
output =
[ show xPair
, show yPair ]
-- Paste into http://try.purescript.org.
module Main where
import Prelude
import Control.Monad.Eff (Eff)
import Data.Foldable (fold)
import TryPureScript (DOM, h1, h2, p, text, list, indent, link, render, code)
import Data.Tuple (Tuple(..))
import Data.Foldable (foldMap)
newtype Point a = Point {x :: a, y :: a}
getx (Point {x}) = x
gety (Point {y}) = y
instance functorPoint :: Functor Point where
map f (Point {x, y}) = Point {x: f x, y: f y}
instance applyPoint :: Apply Point where
apply (Point f) (Point p) = Point {x: f.x p.x, y: f.y p.y}
pointValues = Point {x: 3, y: 4}
pointLabels = Point {x: "x", y: "y"}
xPair = getx $ Tuple <$> pointLabels <*> pointValues
yPair = gety $ Tuple <$> pointLabels <*> pointValues
-- You don't really need getPair anymore, but here it is anyway.
getPair :: forall f a b c. Apply f => (f (Tuple a b) -> c) -> f a -> f b -> c
getPair get p1 p2 = get $ Tuple <$> p1 <*> p2
-- getPair can in fact take a non-getter as the get function ...
getxy (Point {x, y}) = Tuple x y
sumPair = getPair getxy pointLabels pointValues
-- The type might seem very general, not even returning a tuple. This is basically
-- because you have f a -> f b -> f (Tuple a b) composed with f (Tuple a b) -> c.
main :: Eff (dom :: DOM) Unit
main =
render $ foldMap (p <<< text) output
where
output =
[ show xPair
, show yPair
, show sumPair ]

So, in summary, we can have at least these three different types for our getPair function:

getPair :: forall a b. (a -> b) -> a -> a -> Tuple b b
getPair :: forall t a b. (forall c. t c -> c) -> t a -> t b -> Tuple a b
getPair :: forall f a b c. Apply f => (f (Tuple a b) -> c) -> f a -> f b -> c
  • In the first, the points need to be of the same type.
  • In the second, it must be placed in a wrapper type, and you must write the type yourself.
  • In the third, Point must implement Apply, but the type is inferred and you can use a more general function than a getter, like getting both x and y.

The downside with the last, using Apply is that you do the computations for all fields and then just extract one. In a lazy language that would not be a problem, but PureScript is strict.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment