Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to restrict a DU/ADT to certain case identifiers/value constructors

How would I approach the following situatiuon?
I have a DU (for example a Currency) and some record type. Now for the record type fields I have the requierements that the actual values for a given instance should al be of the same case identifier (or in Haskell the same value constructor)

type Currency =
    | USD of decimal
    | EUR of decimal

type PositionalData = {
    grossAmount: Currency;
    pos1: Currency;
    pos2: Currency;
}

for example the following is valid

let valid = {
    grossAmount = USD 10.0m;
    pos1 = USD 7.0m;
    pos2 = USD 3.0m;
}

where as this example should be invalid

let wrong = {
    grossAmount = USD 10.0m;
    pos1 = USD 7.0m;
    pos2 = EUR 3.0m;
    ^^^^^^^^^^^^^^^^
}

I know this particular example can be solved in F# using Units of Measurement. But its easy to envision an example that is not solvable via that mechanism. So I like to ask you to consider a more generic answer and not necessarily one that just solves the give code example.

Looking forward to your brain dumps ;-)

PS: for all the Haskeleers around - It would be interesting to see how ADT (maybe in combination wit higher kinded types) can solve that.

like image 916
robkuz Avatar asked Dec 24 '22 03:12

robkuz


1 Answers

A "direct" translation could be

{-# LANGUAGE GADTs, DataKinds, KindSignatures #-}

data Currency = USD | EUR deriving Show

-- We use `Currency` values to create `Amount` types
--   read about types in Haskell ([Kinds][1]: *, * -> *, ...)
--   here we fix one * to be Currency
data Amount :: Currency -> * where
     -- Data constructor, take one float and return some Amount
     Amount :: Float -> Amount a

-- Extract the specific currency symbol require extra effort
instance Show (Amount a) where
    show (Amount k) = show k

-- Many amounts (same currency)
-- `a` restrict `a1` and `a1` to have the same type => the same currency
data PData a = PData { a1 :: Amount a
                     , a2 :: Amount a
                     } deriving Show

-- Helpers
usd :: Float -> Amount USD
usd = Amount

eur :: Float -> Amount EUR
eur = Amount

main = do

    print $ PData (usd 3) (usd 4)  -- OK
    print $ PData (eur 3) (eur 4)  -- OK
    print $ PData (eur 3) (usd 4)  -- KO, Couldn't match type 'USD with 'EUR

(1) https://wiki.haskell.org/Kind

On the other hand, @TheInnerLight remember me you can use phantom types

-- NOTE: this is not a "direct translation" since currencies are not
--       enumerated and is slightly different
data USD = USD
data EUR = EUR

data Amount c = Amount { amount :: Float }

instance Show (Amount c) where
    show (Amount a) = show a

data PData c = PData { c1 :: Amount c
                     , c2 :: Amount c }
                       deriving Show

usd :: Float -> Amount USD
usd = Amount

eur :: Float -> Amount EUR
eur = Amount

main = do

    print $ PData (usd 3) (usd 4)  -- OK
    print $ PData (eur 3) (eur 4)  -- OK
    print $ PData (eur 3) (usd 4)  -- KO, Couldn't match type 'USD with 'EUR

One way to extract currency symbols (or any other data) could be

class    Symbol c   where symbol :: c -> String
instance Symbol USD where symbol _ = "USD"
instance Symbol EUR where symbol _ = "EUR"

instance Symbol c => Show (Amount c) where
    show s@(Amount a) = sym undefined s ++ " " ++ show a
                        where sym :: Symbol c => c -> Amount c -> String
                              sym k _ = symbol k

printing

PData {c1 = USD 3.0, c2 = USD 4.0}
PData {c1 = EUR 3.0, c2 = EUR 4.0}
like image 160
josejuan Avatar answered Dec 28 '22 05:12

josejuan