7

I was given a puzzle to do the following in Haskell,

f takes two functions, function a and function b. Function a takes na inputs and returns a Num type and function b takes nb inputs and returns a Num type. f returns a new function of arity na+nb that applies a to the first na arguments, nb to the rest of the arguments and returns their sum.

In mathematics I would write this as:

latex of formula

My first naïve attempt at this in Haskell was:

f a b = flip ((+) . a) . b

But this only works if a is a unary function.

After this I thought about the puzzle for a long while without being able to come up with even an idea for how I might do this. This is the first time in a long time I have been this utterly stumped in Haskell.

How might I solve this puzzle? Is there a solution to this puzzle? (I was given this puzzle by a friend and I don't believe they had a actual solution in mind at the time)

13
  • Can we have an example in Haskell because there are two ways to interpret $f(a,b)$ in Haskell (i.e. do you want e.g. f a b :: (Int,Int,Int) -> Int or f a b :: Int -> Int -> Int?) Commented Dec 26, 2017 at 1:56
  • The type of a function taking n_a inputs is Vec n_a X -> Y where data Vec n a where Nil :: Vec 0 a; Cons ::a -> Vec n a -> Vec (1 + n) a. Commented Dec 26, 2017 at 2:01
  • @DanRobertson Well strictly speaking we won't know f a b to have either of those types, because f a b might take any number of inputs. However I think the intention of the puzzle is for a curried function, because having a tuple of arbitrary size is clearly impossible in Haskell. (That being said currying in this way might be impossible, I'm just not sure of that in the way I am sure taking arbitrary tuples won't work) Commented Dec 26, 2017 at 2:01
  • Yes of course. I was using an arbitrary example to clarify whether you wanted currying or not or something odd like Vec above or some kind of typed homogeneous tuple-list hybrid. That clears things up. Commented Dec 26, 2017 at 2:03
  • @DanRobertson The puzzle was given to me as is, so I'd be interested in both/any type of solution. Currying would be cool, but a solution is a solution, and I'm certainly not going to complain because I find the puzzle interesting, and new ways of thinking are probably good for me. Commented Dec 26, 2017 at 2:07

2 Answers 2

7

Here's a pretty simple approach using type families that works monomorphically in the numeric type (e.g., specialized to Int). We'll need a few extensions:

{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE InstanceSigs #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE TypeFamilies #-}

The function f will be defined in a type class:

class VarArgs r s where
  type F r s
  f :: r -> s -> F r s

and we'll handle the following cases. If the type of the first function is of form a :: Int -> r, we'll use the following instance to gobble an argument x and feed it to a:

instance VarArgs r s => VarArgs (Int -> r) s where
  type F (Int -> r) s = Int -> F r s
  f :: (Int -> r) -> s -> Int -> F r s
  f a b x = f (a x) b

This has the effect of recursing on the type of a until it's of the form Int. Then, we'll use a similar instance to recurse on the type b :: Int -> s:

instance VarArgs Int s => VarArgs Int (Int -> s) where
  type F Int (Int -> s) = Int -> F Int s
  f :: Int -> (Int -> s) -> Int -> F Int s
  f a b x = f a (b x)

Ultimately, both functions will be reduced to 0-ary functions of type a, b :: Int, and we can use the terminal instance:

instance VarArgs Int Int where
  type F Int Int = Int
  f :: Int -> Int -> Int
  f a b = a + b

Here's a little test to prove it works:

times2 :: Int -> Int -> Int
times2 x y = x * y
times3 :: Int -> Int -> Int -> Int
times3 x y z = x * y * z

foo :: [Int]
foo = [ f times2 times2 1 2 3 4
      , f times2 times3 1 2 3 4 5
      , f times3 times2 1 2 3 4 5
      , f times3 times3 1 2 3 4 5 6]

and loading this into GHCi gives:

> foo
[14,62,26,126]
>

Generalizing this to be polymorphic in any Num type doesn't seem to be straightforward. Replacing the Int type with a constrained Num n type leads to errors regarding conflicting family instance declarations.

Sign up to request clarification or add additional context in comments.

3 Comments

Upvoted, but I wouldn't call this a "pretty simple approach" ;-) It's not hard, but it's not exactly beginner's material.
Another small problem with this is that you need times2. Why not just (*)? f (*) (*) 1 2 3 4 will produce a stream of errors, because the type of (*) is too good for f.
I suspect you're better off counting arrows than looking for Int. You'll miss Num instances for functions, but catch the rest.
2

This is easy and simple - much simpler than @K.A.Buhr's type family approach, in my opinion - if you tweak your representation of an n-ary function, instead using a unary function of an n-dimensional vector.

{-# LANGUAGE GADTs #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}

import Prelude hiding (splitAt)
import Data.Bifunctor

The usual suspects: (type-level) natural numbers, their (value-level) singletons, type-level addition, and vectors.

data Nat = Z | S Nat
data Natty n where
    Zy :: Natty Z
    Sy :: Natty n -> Natty (S n)
type family n + m where
    Z + m = m
    S n + m = S (n + m)

data Vec n a where
    Nil :: Vec Z a
    (:>) :: a -> Vec n a -> Vec (S n) a

splitAt takes a runtime Natty - it needs to know at runtime where to split the vector - and a vector that is at least as long as the Natty.

splitAt :: Natty n -> Vec (n + m) a -> (Vec n a, Vec m a)
splitAt Zy xs = (Nil, xs)
splitAt (Sy n) (x :> xs) =
    let (ys, zs) = splitAt n xs
    in (x :> ys, zs)

Then your f, which I'm calling splitApply, is a straightforward application of splitAt.

splitApply :: Natty n -> (Vec n a -> b) -> (Vec m a -> c) -> Vec (n + m) a -> (b, c)
splitApply at f g xs = bimap f g $ splitAt at xs

(I haven't bothered to show the "add the results" part, because it's so simple that I bored myself writing it. You could argue that, since Hask is a monoidal category, (,) represents a sort of addition anyway.)

1 Comment

I think an HList probably fits better than a vector--the problem description doesn't indicate that the function arguments all have the same type. The whole thing works out about the same, except you have ++ instead of +. But to get maximal type inference, you probably also need to define dropping from both the right and the left.

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.