Why is pattern preference preferable in function definitions?

I am reading the learnyouahaskell tutorial from learnyouahaskell . There he reads:

Pattern matching can also be used for tuples. What if we want to make a function that takes two vectors in two-dimensional space (which have the form of pairs) and adds them together? To add two vectors, we add their components x separately, and then their components y separately. Here's how we would do it if we didn’t know about template matching:

 addVectors :: (Num a) => (a, a) -> (a, a) -> (a, a) addVectors ab = (fst a + fst b, snd a + snd b) 

Well, it works, but there is a better way to do it. Let modify to use pattern matching.

 addVectors :: (Num a) => (a, a) -> (a, a) -> (a, a) addVectors (x1, y1) (x2, y2) = (x1 + x2, y1 + y2) 

We go! Much better. Please note that this is already a template. Type addVectors (in both cases) addVectors :: (Num a) => (a, a) -> (a, a) - > (a, a) , so we are guaranteed to get two pairs as parameters.

My question is: why is pattern matching preferable if both definitions result in the same signature?

+6
source share
3 answers

I think that in this case, pattern matching expresses more directly what you mean.

In the case of a function application, you need to know what fst and snd , and from it it is deduced that a and b are tuples whose elements are added.

 addVectors ab = (fst a + fst b, snd a + snd b) 

The fact that we have snd and fst for tuple decomposition is distracting here.

In the case of pattern matching, it immediately becomes clear what the input is (a tuple whose elements we call x1 and y1 and a tuple ... etc.) and how it is deconstructed. And you can immediately see what happens as their elements are added.

 addVectors (x1, y1) (x2, y2) = (x1 + x2, y1 + y2) 

It almost looks like a mathematical definition:

(x 1 , y 1 ) + (x 2 , y 2 ): = (x 1 + x 2 , y 1 + y 2 )

Right to the point, without distractions :-)

You can literally write this in Haskell:

 (x₁, y₁) `addVector` (xβ‚‚, yβ‚‚) = (x₁ + xβ‚‚, y₁ + yβ‚‚) 
+5
source

In a nutshell, you need to build and destroy values.

Values ​​are constructed by adopting a data constructor, which is a (possibly null) function and applies the required arguments. So far so good.

Random example (abuse of GADTSyntax )

 data T where A :: Int -> T B :: T C :: String -> Bool -> T 

Destruction is more complicated, because you need to take a value of type T and get information about 1) which constructor was used to create such a value, and 2) what arguments to the specified constructor.

Part 1) can be performed using the function:

 whichConsT :: T -> Int -- returns 0,1,2 for A,B,C 

Part 2) is more complicated. A possible option is to use forecasts

 projA :: T -> Int -- projB not needed projC1 :: T -> String projC2 :: T -> Bool 

so that, for example, they satisfy

 projA (A n) = n projC1 (C xy) = x projC2 (C xy) = y 

But wait! Projection types have the form T -> ... , which promises that such functions work on all values ​​of type T So we can have

 projA B = ?? projA (C xy) = ?? projC1 (A n) = ?? 

How to implement the above? There is no way to get reasonable results, so the best option is to run a runtime error.

 projA B = error "not an A!" projA (C xy) = error "not an A!" projC1 (A n) = error "not a C!" 

However, this puts a strain on the programmer! Now the programmer's responsibility is to verify that the values ​​that are passed to the projections have the correct constructor. This can be done using whichConsT . Many imperative programmers are used for this type of interface (testing and access, for example Java hasNext(), next() in iterators), but this is due to the fact that most imperative languages ​​do not have a really better option.

FP languages ​​(and, currently, some imperative languages) also allow you to map patterns. Using it has the following advantages over projections:

  • No need to share information: we get 1) and 2) at the same time
  • Program cannot be minimized: we never use partial projection functions that may be damaged
  • no load on the programmer: a consequence of the above
  • If integrity checking is enabled, we will certainly consider all possible cases.

Now that the types have exactly one constructor (tuples, () , newtype s), you can define general projections that work great (for example, fst,snd ). However, many prefer to stick to pattern matching, which can also handle the general case.

+3
source

Like Carsten mentioned in the comments, this is an opinion-based question, but let me elaborate.

Using template matching with 2 tuples is not so much, but consider some large data structure, for example, 4 tuples.

 addVectors :: (Num a) => (a, a, a, a) -> (a, a, a, a) -> (a, a, a, a) addVectors ab = -- some code that adds vectors addVectors :: (Num a) => (a, a, a, a) -> (a, a, a, a) -> (a, a, a, a) addVectors (w1, x1, y1, z1) (w2, x2, y2, z2) = (w1 + w2, x1 + x2, y1 + y2, z1 + z2) 

Without matching patterns, you will have to write functions that extract the first, second, third, and fourth elements from 4 tuples and use it inside addVectors . When matching with a pattern, the addVectors implementation addVectors very simple.

I believe that using such an example in a book could receive a message more efficiently.

+2
source

All Articles