2

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!

5
  • 1
    Does this answer your question? How to create a polyvariadic haskell function? Commented Apr 26, 2020 at 12:36
  • Thanks for the suggestion! I'm aware of the general strategy, but I'm not sure how to make it work in this particular instance because the variadic function has a relationship to other arguments. In the case above, Haskell cannot figure out that a should be an arbitrary argument that eltMaker can consume in order to create an NAble b. Commented Apr 26, 2020 at 13:26
  • This reminds me a bit of gelisam’s n-ary functors — you might find that link useful (although you might not, I’m not sure if it’s quite the same thing) Commented Apr 26, 2020 at 13:43
  • Another thought: if you define a heterogeneous list, it might be easier to write a variadic function of the form HList '[s -> a, s -> b, s -> c, …] -> (HList '[a, b, c, …] -> Node) -> s -> Node. Again, not sure if this will make your task any easier, but I think it’s worth a try. Commented Apr 26, 2020 at 13:45
  • I had thought about HLists but unfortunately I wasn't able to succeed with them. It could be due to my unfamiliarity with HLists, though. Commented Apr 26, 2020 at 14:16

1 Answer 1

3

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
Sign up to request clarification or add additional context in comments.

2 Comments

(Actually, most people use mkNode <$> f1 <*> f2 <*> ... <*> fn. But I think the symmetry of my version is better; e.g. if the order of arguments to mkNode changes, the edit to swap lines doesn't need a followup edit to swap operators in some cases.)
Really nice! I will definitely use the applicative pattern defined in the standard library. ``` data FNode = MkNode; data MockState = MockState; ms2t :: MockState -> Text; ms2t _ = "a"; ms2i :: MockState -> Int; ms2i _ = 1; res = (\x y -> MkNode) <$> ms2i <*> ms2t ```

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.