Question

If I have two lists of identical size in Haskell

list1 = [1.0,2.0,3.0]
list2 = [3.0,5.0,7.0]

how would I perform element-wise addition to create a third list of equal size?

[4.0,7.0,10.0]

Specifically, I want to make a function like this:

listAdd :: [Float] -> [Float] -> [Float]
listAdd a b
    | length a /= length b = error "length mismatch"
    otherwise              = ????

I can't figure out what to put in place of '????'. I think it must involve 'map', and some version of '+', but the partial evaluation thing is confusing me, and the correct syntax has proved elusive.

EDIT 1:

I thought I understood the pattern matching with the cons operator, so I tried this next:

listAdd :: [Float] -> [Float] -> [Float]
listadd (x:xs) (y:ys) = (x+y) : listAdd xs ys
listAdd []     []     = []
listAdd _      _      = error "length mismatch"

But there is still something wrong, as

listAdd [1.0,2.0] [2.0,3.0]

goes right past the useful patterns and returns an error.

EDIT 2:

With the typo eliminated,

listAdd :: [Float] -> [Float] -> [Float]
listAdd (x:xs) (y:ys) = (x+y) : listAdd xs ys
listAdd []     []     = []
listAdd _      _      = error "length mismatch"

works as advertised. As managing types for tensors of arbitrary dimension is beyond me at this early point in my studies, I decided to merely extend it to matrix addition:

mAdd :: [[Float]] -> [[Float]] -> [[Float]]
mAdd (x:xs) (y:ys) = listAdd x y : mAdd xs ys
mAdd []     []     = []
mAdd _      _      = error "length mismatch"

I realize it's probably not ideal to tie the functions together, as it reduces modularity/portability, but it does what I need it to do.

EDIT 3:

I hope it's not premature to declare that some degree of learning has occurred. I now have this:

listCombine :: (Float -> Float -> Float) -> [Float] -> [Float] -> [Float]
listCombine f (x:xs) (y:ys) = (f x y) : listCombine f xs ys
listCombine f []     []     = []
listCombine _ _      _      = error "length mismatch"

which is probably identical to zipWith, except that it gives an error for mismatched lengths. It handled the corner cases I threw at it with expected results.

Was it helpful?

Solution

Separately calculating the argument lists' length is a bad idea. We usually want to consume as little of the input as possible while producing as much of output as possible. This is known as "not forcing the input too much", i.e. being as lazy as possible.

In your case, when we analyze the two arguments we'll know about the length mismatch if one list is empty and the other isn't:

listAdd :: [Float] -> [Float] -> [Float]
listAdd (x:xs) (y:ys) =  (x+y) : listAdd ... ...
listAdd []     []     = []
listAdd _      _      =  error "length mismatch"

This way it will work even for infinite lists.

The built-in function similar to this is zipWith, but it ignores the list length mismatch:

Prelude> zipWith(+) [1,2] [3]
[4]

It is equivalent to the above definition with the last two lines replaced with the catch-all clause

listAdd _      _      = []

OTHER TIPS

This doesn't address the original question but one of the comments: addition of tensors. My understanding of tensors is as n-dimensional matrices. You already know that element-wise addition is just an application of zipWith. But we can write the function addLists using the applicative and functor typeclasses:

import Control.Applicative

addLists :: [Float] -> [Float] -> [Float]
addLists x y = (*) <$> x <*> y

or equivalently:

addLists :: [Float] -> [Float] -> [Float]
addLists = liftA2 (*) 

Note that <$> == fmap and <*> is in the Applicative class.

First we define a type for rank-0 tensors:

newtype Identity a = Identity {getIdentity :: a}

instance Functor Identity where 
  fmap f (Identity a) = Identity (f a) 

instance Applicative Identity where 
  pure a = Identity a
  (<*>) (Identity f) (Identity a) = Identity (f a)

Then a type for adding a new layer on top of a rank-n tensor, creating a rank n+1 tensor:

newtype Compose f g a = Compose {getCompose :: f (g a)}

instance (Functor f, Functor g) => Functor (Compose f g) where 
  fmap f (Compose a) = Compose (fmap (fmap f) a)

instance (Applicative f, Applicative g) => Applicative (Compose f g) where 
  pure a = Compose (pure (pure a))
  (<*>) (Compose f) (Compose a) = Compose (liftA2 (<*>) f a)

But wait, there's more! These type and their instances are already defined for you, namely in Data.Functor.Identity and Data.Functor.Compose. You can now write generic tensor element-wise addition:

addTensor :: Applicative f => f Float -> f Float -> f Float
addTensor = liftA2 (*) 

The function liftA2 just generalizes zipWith to Applicatives. For rank-1 you have:

addTensor1 :: Compose [] Identity Float -> Compose [] Identity Float -> Compose [] Identity Float
addTensor1 = addTensor

This type is a bit noisy. You can easily define type synonyms:

type Tensor0 = Identity
type Tensor1 = Compose [] Tensor0
type Tensor2 = Compose [] Tensor1
type Tensor3 = Compose [] Tensor2
type Tensor4 = Compose [] Tensor3

Then:

addTensor3 :: Tensor3 Float -> Tensor3 Float -> Tensor3 Float
addTensor3 = addTensor

addTensor4 :: Tensor4 Float -> Tensor4 Float -> Tensor4 Float
addTensor4 = addTensor

Since addTensor abstracts over all Applicatives you may want to define a subclass of Applicative which form valid tensors:

class Applicative t => Tensor t 
instance Tensor Identity
instance Tensor t => Tensor (Compose [] t)

addTensor :: Tensor f => f Float -> f Float -> f Float
addTensor = liftA2 (*) 

I suspect you may have other operations on tensors other than addition. This general form makes it very easy to define a large range of operations. But hypothetically if you only wanted addTensor you could do something like:

class Tensor a where 
  addTensor :: a -> a -> a 

instance Tensor Float where 
  addTensor = (+)

instance Tensor t => Tensor [t] where 
  addTensor = zipWith addTensor 

Much simpler types result: addTensor :: [[[[Float]]]] -> [[[[Float]]]] -> [[[[Float]]]] for rank-4.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top