Mike
Mike

Reputation: 796

Using a typeclass for variadic argument pattern in Haskell

Let's say I have a convention in Haskell where I define a series of functions like this:


data Node = MkNode

s0 :: Node -> s -> Node
s0 a _ = a

s1 :: (s -> a) -> (a -> Node) -> s -> Node
s1 a b c = b (a c)

s2 :: (s -> a) -> (s -> b) -> (a -> b -> Node) -> s -> Node
s2 a b c d = c (a d) (b d)

s3 :: (s -> a) -> (s -> b) -> (s -> c) -> (a -> b -> c -> Node) -> s -> Node
s3 a b c d e = d (a e) (b e) (c e)

If possible, I would love to define a function sn that takes a variable number of arguments, always with this pattern. I've seen this sort of thing done before using typeclasses, but I can't quite figure out how to do it in this case. For example, I can imagine:

class NAble elt where
    sn :: elt -> state -> Node

instance NAble Node where
    sn elt _ = elt

But then I'm stuck. I'm not sure what the recursive definition would be. Perhaps something like:

instance (NAble b) => NAble (a -> b) where
    sn eltMaker state = ss (eltMaker state) state

But that's obviously not quite right. Not sure if this is possible, but it would be cool if it were. Of course the order of the arguments can change if that helps get it right, but it'd be really nice to get this to work. Any help would be appreciated!

Upvotes: 2

Views: 86

Answers (1)

Daniel Wagner
Daniel Wagner

Reputation: 152707

If you put the arguments in a slightly different order -- with the s argument coming first, and the Node-constructing function second -- it gets a lot easier. Then a type family will fix you right up:

{-# LANGUAGE TypeFamilies #-}

data Node = MkNode

class NAble t where
    type Ret t s
    sn :: s -> t -> Ret t s

instance NAble Node where
    type Ret Node s = Node
    sn s mkNode = mkNode

instance NAble t => NAble (a -> t) where
    type Ret (a -> t) s = (s -> a) -> Ret t s
    sn s mkNode fa = sn s (mkNode (fa s))

But let me also recommend an alternative. Look to the pattern the standard library uses:

pure   :: Applicative f => (               t)                      -> f t
fmap   :: Applicative f => (a ->           t) -> f a               -> f t
liftA2 :: Applicative f => (a -> b ->      t) -> f a -> f b        -> f t
liftA3 :: Applicative f => (a -> b -> c -> t) -> f a -> f b -> f c -> f t

Taking f~(->) s and t~Node, we get:

pure   :: (               Node)                                     -> s -> Node
fmap   :: (a ->           Node) -> (s -> a)                         -> s -> Node
liftA2 :: (a -> b ->      Node) -> (s -> a) -> (s -> b)             -> s -> Node
liftA3 :: (a -> b -> c -> Node) -> (s -> a) -> (s -> b) -> (s -> c) -> s -> Node

What if people who use the standard library need liftA4 or higher? Typically they will then switch to a chain of (<*>) uses instead:

(<*>) :: (s -> a -> Node) -> (s -> a) -> s -> Node
(f <*> g) s = f s (g s)

{-# MAKE_THE_PROGRAMMER_INLINE liftAn #-}
liftAn mkNode f1 f2 ... fn = pure mkNode
    <*> f1
    <*> f2
    ...
    <*> fn

Upvotes: 3

Related Questions