My question is related to a more general question on Haskell program design. But I would like to focus on a specific use case.
I defined a data type (e.g. Foo
), and used it in a function (e.g. f
) through pattern matching. Later, I realized that the type (Foo
) requires some additional field to support new functionalities. However, adding the field would change how the type can be used; i.e. the existing functions depending on the type could be affected. Adding new functionalities to existing code, however unappealing, is hard to avoid. I am wondering what are the best practices at the Haskell language level to minimize the impact of such kind of modifications.
For example, the existing code is:
data Foo = Foo {
vv :: [Int]
}
f :: Foo -> Int
f (Foo v) = sum v
The function f
will be syntax wrong if I add another field to Foo
:
data Foo = Foo {
vv :: [Int]
uu :: [Int]
}
However, if I had defined function f
as the following in the first place:
f :: Foo -> Int
f foo = sum $ vv foo
, then even with the modification on Foo
, f
would still be correct.
The Data Keyword and Constructors In general, we define a new data type by using the data keyword, followed by the name of the type we're defining. The type has to begin with a capital letter to distinguish it from normal expression names. To start defining our type, we must provide a constructor.
So no, Haskell types do not exist at runtime, in any form.
Type and data type refer to exactly the same concept. The Haskell keywords type and data are different, though: data allows you to introduce a new algebraic data type, while type just makes a type synonym. See the Haskell wiki for details.
In Haskell, every statement is considered as a mathematical expression and the category of this expression is called as a Type. You can say that "Type" is the data type of the expression used at compile time.
Lenses solve this problem well. Just define a lens that points to the field of interest:
import Control.Lens
newtype Foo = Foo [Int]
v :: Lens' Foo [Int]
v k (Foo x) = fmap Foo (k x)
You can use this lens as a getter:
view v :: Foo -> [Int]
... a setter:
set v :: [Int] -> Foo -> Foo
... and a mapper:
over v :: ([Int] -> [Int]) -> Foo -> Foo
The best part is that if you later change your data type's internal representation, 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 only used the lens to interact with your Foo
then you won't break backwards compatibility.
The best practice for processing types that might get new fields added that you want to ignore in existing code is indeed to use record selectors as you've done.
I would say that you should always define any type that might change using record notation, and you should never pattern match on a type defined with record notation using the first style with positional arguments.
Another way of expressing the above code is:
f :: Foo -> Int
f (Foo { vv = v }) = sum v
This is arguably more elegant, and it also works better in the case where Foo
has multiple data constructors.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With