Very often when I write something using Haskell I need records with multiple constructors. E.g. I want to develop some kind of logic schemes modelling. I came up to such type:
data Block a = Binary {
binOp :: a -> a -> a
, opName :: String
, in1 :: String
, in2 :: String
, out :: String
} | Unary {
unOp :: a -> a
, opName :: String
, in_ :: String
, out :: String
}
It describes two types of blocks: binary (like and, or etc.) and unary (like not). They contain core function, input and output signals.
Another example: type to describe console commands.
data Command = Command { info :: CommandInfo
, action :: Args -> Action () }
| FileCommand { info :: CommandInfo
, fileAction :: F.File -> Args -> Action ()
, permissions :: F.Permissions}
FileCommand needs additional field - required permissions and its action accept file as a first parameter.
As I read and search topics, books etc. about Haskell, it seems that it is not common to use types with record syntax and many constructors simultaneously.
So the question: is this "pattern" is not haskell-way and why? And if it is so, how to avoid it?
P.S. Which from proposed layouts is better, or maybe there is more readable one? Because I can't find any examples and suggestions in other sources.
I would recommend not using ADTs and record types at the same time, simply for the reason that unOp (Binary (+) "+" "1" "2" "3")
type checks without warning with -Wall
, but will crash your program. It essentially circumvents the type system and I personally think that feature should be removed from GHC or you should have to make each constructor have the same fields.
What you're wanting is a sum type of two records. This is perfectly achievable and much safer with Either
, and requires about as much boilerplate since you'd have to write isBinaryOp
and isUnaryOp
functions anyway to mirror isLeft
or isRight
. Additionally, Either
has many functions and instances that make working with it easier, while your custom type does not. Just define each constructor as its own type:
data BinaryOp a = BinaryOp
{ binOp :: a -> a -> a
, opName :: String
, in1 :: String
, in2 :: String
, out :: String
}
data UnaryOp a = UnaryOp
{ unOp :: a -> a
, opName :: String
, in_ :: String
, out :: String
}
type Block a = Either (BinaryOp a) (UnaryOp a)
data Command' = Command
{ info :: CommandInfo
, action :: Args -> Action ()
}
data FileCommand = FileCommand
{ fileAction :: F.File -> Args -> Action ()
, permissions :: F.Permissions
}
type Command = Either Command' FileCommand
This isn't really much more code, and it's isomorphic to your original types while taking full advantage of the type system and available functions. You can also easily write equivalent functions between the two:
-- Before
accessBinOp :: (Block a -> b) -> Block a -> Maybe b
accessBinOp f b@(BinaryOp _ _ _ _ _) = Just $ f b
accessBinOp f _ = Nothing
-- After
accessBinOp :: (BinaryOp a -> b) -> Block a -> Maybe b
accessBinOp f (Left b) = Just $ f b
accessBinOp f _ = Nothing
-- Usage of the before version
> accessBinOp in1 (BinaryOp (+) "+" "1" "2" "3")
Just "1"
> accessBinOp in_ (BinaryOp (+) "+" "1" "2" "3")
*** Exception: No match in record selector in_
-- Usage of the after version
> accessBinOp in1 (Left $ BinaryOp (+) "+" "1" "2" "3")
Just "1"
> accessBinOp in_ (Left $ BinaryOp (+) "+" "1" "2" "3")
Couldn't match type `UnaryOp a1` with `BinaryOp a0`
Expected type: BinaryOp a0 -> String
Actual type: UnaryOp a1 -> String
...
So before, you get an exception if you use a nontotal function, but afterwards you only have total functions and can restrict your accessors so that the type system catches your errors for you, rather than the runtime.
One key difference is not f
can be restricted to only working on
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