These are simple representations of sample Input and Output types:
Input = I1 Int | I2 String
Output = OA String | OB Bool | OC
Parameters here are just for the sake of greater realism. :)
I would like to get a function that maps Input
to Output
:
inputToOutput = \case
I1 val -> OA (show val)
I2 str -> (OB (str == "x"))
But this function should be typed checked against allows maps (I1 -> OB
,I2 -> OB
), so I could NOT do:
inputToOutput = \case
I1 val -> OB True -- allows only OA
I2 str -> OA str -- allows only OB
I have a solution variant with GADTs and Families, here I have to introduce additional tags for each of Input
and Output
values:
-- Input tags
data I_ = I1_ | I2_
data Input (i :: I_) where
I1 :: Int -> Input 'I1_
I2 :: String -> Input 'I2_
-- Output tags
data O_ = OA_ | OB_ | OC_
data Output (o :: O_) where
OA :: String -> Output 'OA_
OB :: Bool -> Output 'OB_
OC :: Output 'OC_
-- Input/Output rules for tags
type family I2O (i :: I_) :: O_ where
I2O 'I1_ = 'OA_
I2O 'I2_ = 'OB_
inputToOutput :: Input a -> Output (I2O a)
inputToOutput = \case
I1 val -> OA (show val)
I2 str -> (OB (str == "x"))
My first question: is this the only way to achieve what I want (Using GADTs/Families)? what I don't like there, that I need to introduce additional boilerplate tags for Input/Output
values, is it possible to make it less boilerplaty way?
My second question: is a more advanced logic of mapping possible? Say I want the input to be mapped to a list of outputs that is required to contain one/serveral of the values:
inputToOutput = \case
I1 val -> [ OA (show val), OC] -- also allows other Output values like OC, but not requires
I2 str -> [(OB (str == "x"))]
The simplest way to achieve your goal seems to be this, which is also what I would probably use in practice: just implement two functions
i1case :: Int -> String
i2case :: String -> Bool
and then make it
inputToOutput = \case
I1 val -> OA $ i1case val
I2 str -> OB $ i2case str
If using the type of the contained elements as a sanity check is too weak, you can always add newtype wrappers, which would perhaps be a good idea anyway.
This still has some limitations compared to your tags. That approach could certainly make sense too. It requires some boilerplate, yes. In principle you don't actually need to define fresh types for the tags, you could also just use
data Input (i :: Symbol) where
I1 :: Int -> Input "I1"
I2 :: String -> Input "I2"
etc., but I wouldn't actually recommend this.
As for your second question,
is a more advanced logic of mapping possible? Say I want the input to be mapped to a list of outputs that is required to contain one/serveral of the values
– well, instead of trying to require a list to “contain one” or more, you could simply require that one right there and then, plus additional a list of outputs whose type you don't care about. For the latter you can use a simple existential wrapper,
data SomeOutput where
SomeOutput :: Output o -> SomeOutput
and then
inputToOutput :: Input a -> (Output (I2O a), [SomeOutput])
inputToOutput = \case
I1 val -> (OA (show val), [SomeOutput OC])
I2 str -> (OB (str == "x"), [])
More complicated requirements, like “at most three of this constructor”, are possible too, but this would get rather painful to express. Starting from some
data ORequirement = AtLeast Nat O_
| AtMost Nat O_
...
type ORequirements = [ORequirement]
type family IOReq (i :: I_) :: OREquirements where
IOReq 'I1_ = '[ 'AtLeast 1 'O1_ ]
...
type family ConformsRequirements (rs :: ORequirements)
(os :: [O_])
:: Constraint where
...
data CertainOutputs (os :: [O_]) where
Nil/Cons...
inputToOutput :: ConformsRequirements (IOReq a) os
=> Input a -> CertainOutputs os
inputToOutput = ...
you might get it to work, but you'd need to use of a lot of singletons
machinery to make it feasible. I'd probably not bother, and rather check the invariants with QuickCheck instead. Types are great, but their true strength is when they can actually help finding the right solutions and express intent. When you just want to forbid some kinds of code, types aren't a very effective tool, at least not in Haskell. In case you really need water-tight proofs, it's not the right language anyway, use Agda / Coq / Idris / Lean. If you're OTOH ok with merely reasonably high probability of catching any violations, QuickCheck gets the job done so much easier.