Haskell Best Practices for Designing and Using a Data Type

My question is related to the more general question of developing a Haskell program. But I would like to dwell on a specific precedent.

I defined a data type (e.g. Foo ) and used it in a function (e.g. f ) using pattern matching. Later I realized that the type ( Foo ) requires some extra field to support the new functionality. However, adding a field will change the way this type is used; those. Existing type-specific functions may be affected. It is impossible to avoid adding new functionality to existing code, no matter how unattractive it is. I am wondering what the best practices at the Haskell language level are to minimize the impact of such modifications.

For example, existing code:

 data Foo = Foo { vv :: [Int] } f :: Foo -> Int f (Foo v) = sum v 

Function f will be a syntax error if I add another field to Foo :

 data Foo = Foo { vv :: [Int] uu :: [Int] } 

However, if I defined the function f as follows:

 f :: Foo -> Int f foo = sum $ vv foo 

then even with a modification to Foo , f will still be correct.

+8
design haskell
source share
3 answers

Lenses solve this problem well. Just identify the lens that points to the field of interest:

 import Control.Lens newtype Foo = Foo [Int] v :: Lens' Foo [Int] vk (Foo x) = fmap Foo (kx) 

You can use this lens as a getter:

 view v :: Foo -> [Int] 

... setter:

 set v :: [Int] -> Foo -> Foo 

... and mapper:

 over v :: ([Int] -> [Int]) -> Foo -> Foo 

The best part is that if you later change the internal representation of the data type, all you have to do is change the implementation of v to point to the new location of the field of interest. If your downstream users used the lens to interact with your Foo , then you will not break the compatibility.

+5
source share

The best practice for processing types that can add new fields that you want to ignore in existing code is really to use record selectors as you did.

I would say that you should always define any type that can change using musical notation, and you should never match a pattern to the type defined with musical notation using the first style with positional arguments.

Another way to express the above code:

 f :: Foo -> Int f (Foo { vv = v }) = sum v 

This is arguably more elegant, and it also works better when Foo has multiple data constructors.

+2
source share

Your f function is so simple that perhaps the easiest answer would be to write it in a dotless style using composition:

 f' :: Foo -> Int f' = sum . vv 

If your function needs more than one field from the Foo value, the above will not work. But we could use the Applicative instance for (->) and do the following trick:

 import Control.Applicative data Foo2 = Foo2 { vv' :: [Int] , uu' :: [Int] } f2 :: Foo2 -> Int f2 = sum . liftA2 (++) vv' uu' 

For functions, liftA2 applies an input argument to two functions, and then combines the results into another function, (++) in this case. But perhaps this borders on the obscure.

+1
source share

All Articles