0

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"))


  1. 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?

  2. 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"))]
WHITECOLOR
  • 24,996
  • 37
  • 121
  • 181
  • when you say `allows only AO` and `allows only AB` do you mean `OA` and `OB`?. Also `I2` constructor doesn't have any argument, but you pattern mathc as `I2 str` – lsmor Feb 27 '23 at 12:17
  • From your question I guess you are triying to do some sort of state machine and use the type system to prevent iligal transitions, isn't it? I am not very confortable with type-level machinery, but there is this post is it helps https://wickstrom.tech/finite-state-machines/2017/11/10/finite-state-machines-part-1-modeling-with-haskell.html – lsmor Feb 27 '23 at 12:23
  • @Ismor Thanks, some typos, GADTs versoin is correct. – WHITECOLOR Feb 27 '23 at 13:07
  • @Ismor the link to the article you posted refers to the problem, but it doesn't provide any advanced type level solutions, though it is discussed in Part 2 of the article. – WHITECOLOR Feb 27 '23 at 13:11

2 Answers2

3

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.

leftaroundabout
  • 117,950
  • 5
  • 174
  • 319
  • Thanks, I would really like to understand if it is feasible and reasonable to try to express more or less complex logic like state transitions with types. – WHITECOLOR Feb 27 '23 at 15:32
  • 2
    @WHITECOLOR well, whether it's feasible depends on how much time/effort you can afford to put into it. My experience is that every venture into dependent types takes a lot more dev time than you'd think, and the investment seldom pays off in terms of objective advantages of the end result over a simpler-typed Haskell solution plus QuickCheck suite. But, if nothing else, dabbling with `singletons` (or a full dependently typed language) is still a good learning experience, so I wouldn't say don't give it a try – but don't expect it to be an exciting adventure, it's mostly boring bookkeeping. – leftaroundabout Feb 27 '23 at 15:41
3

I think for this particular case, is fine with only Output tags which simplifies drastically. The approach is "tag each input and output with the output tag of desire"

data OutputTag = A | B | C

data Input (o :: OutputTag) where 
  I1 :: Int    -> Input A
  I2 :: String -> Input B
  
data Output (o :: OutputTag) where 
  OA :: String -> Output A
  OB :: Bool -> Output B
  OC :: Output C

inputToOutput :: (tag ~ tag') => Input tag -> Output tag'
-- inputToOutput (I1 0) = OC -- Uncomment this line, fails with Couldn't match type ‘'C’ with ‘'A’
inputToOutput (I1 i) = OA (show i)
inputToOutput (I2 s) = OB (s == "x")

here you have the snipet for this code. Probably you can extend the functionality with some not-so-complex type family, but I am not that familiar with this level of haskell. I guess you can do something like

data OutputTag = A | B | C

data Input (o :: [OutputTag]) where 
  I1 :: Int    -> Input [A,C]
  I2 :: String -> Input B
  
data Output (o :: OutputTag) where 
  OA :: String -> Output A
  OB :: Bool -> Output B
  OC :: Output C

-- Here the type family Elem :: OutputTag -> [OutputTag] -> Bool is somthing you have to
-- define or use a library which defines it. I'm not aware of any, but I bet it exists
inputToOutput :: (Elem tag' tags ~ True) => Input tags -> Output tag'
inputToOutput (I1 i) = OA (show i)
inputToOutput (I2 s) = OB (s == "x")
lsmor
  • 4,698
  • 17
  • 38
  • Thanks, It works for this particular sample, but I need a more general way to express dependency logic. – WHITECOLOR Feb 27 '23 at 16:59
  • @WHITECOLOR I guess so, then you are looking for a Dependently typed language, and Haskell isn't one. There are facilities for small use cases, but I think, Implementing some kind of State Machine at the type level is too much – lsmor Feb 27 '23 at 17:18
  • Is there a reason you did not use `inputToOutput :: Input tag -> Output tag` but preferred the `tag~tag'` constraint? – chi Feb 27 '23 at 17:31
  • @chi oh!! hahahaha good point. I was trying to solve the more complex case of `(Elem tag' tags ~ True)` but couldn't find a solution, I guess when copy pasting the code I just simplify to that without realizing – lsmor Feb 27 '23 at 17:41