Chris Stryczynski
Chris Stryczynski

Reputation: 33911

How can I transform a list of values to a data constructor's parameters?

I am processing a web request, and trying to save a potential entity (User / Address 1). The main question is, how can I transform a list of Strings to parameters to a function - where the list could be of arbitrary length.

I have looked at Passing list elements as parameters to curried function but this seems to be a solution only when knowing the amount of parameters in advance.


data User = User String String
data Address1 = Address1 String

let ufields = ["chris", "str"]
let afields = ["earth"]

I'm looking for a function along the lines of:

f :: [String] -> (? -> c) -> Maybe c
f fields c = undefined

So all I would need to pass is the data constructor (User/Address1), and a list of strings.

Examples:

f ufields User would return Just ( User "chris" "str").

f ["chris"] User would return Nothing.

f [] User would return Nothing.

f afields Address1 would return Just ( Address1 "earth" ).

Is this possible to be done without using TemplateHaskell? I can achieve the above manually, but it involves quite a bit of additional typing:

data User = User String String deriving (Show)
data Address1 = Address1 String deriving (Show)

data EntityType = UserEntity | AddressEntity
data EntityContainer = UserContainer User | AddressContainer Address1

f :: EntityType -> [String] -> Maybe EntityContainer
f UserEntity  (p:p':[]) = Just $ UserContainer $ User p p'
f AddressEntity  (p:[]) = Just $ AddressContainer $ Address1 p
f _ _ = Nothing

printPossibleEntity :: [String] -> EntityType -> IO ()
printPossibleEntity fields entityType = 
  case (f entityType fields) of
    Just (UserContainer u) -> print u
    Just (AddressContainer a) -> print a
    Nothing -> print "No entity matched"

main :: IO ()
main = do
  let ufields = ["chris", "str"]
  let afields = ["earth"]
  printPossibleEntity ufields UserEntity
  printPossibleEntity afields AddressEntity
  printPossibleEntity [] AddressEntity

Which outputs:

User "chris" "str"
Address1 "earth"
"No entity matched"

Upvotes: 2

Views: 102

Answers (1)

Alec
Alec

Reputation: 32309

Let me preface this by saying that you should almost certainly not use this.

The usual way of doing this sort of thing is to have overlapping multiparameter typeclasses.

First attempt

{-# LANGUAGE MultiParamTypeClasses, FunctionalDependencies, FlexibleInstances,
             UndecidableInstances #-}

class PackArgs a b | a -> b where
  packArgs :: [String] -> a -> Maybe b

instance {-# OVERLAPPING #-} PackArgs y z => PackArgs (String -> y) z where
  packArgs [] _ = Nothing
  packArgs (a:as) f = packArgs as (f a)

instance {-# OVERLAPPABLE #-} PackArgs z z where
  packArgs (_:_) _ = Nothing
  packArgs [] z = Just z

And here it is working in action:

ghci> data User = User String String deriving Show
ghci> data Address1 = Address1 String deriving Show
ghci> packArgs ["chris","str"] User :: Maybe User
Just (User "chris" "str")
ghci> packArgs ["chris"] User :: Maybe User
Nothing
ghci> packArgs [] User :: Maybe User
Nothing
ghci> packArgs ["earth"] Address1 :: Maybe Address1
Just (Address1 "earth")

The problem is that we need the type annotations for this to work. In a nutshell, Haskell needs to know what your expected return type is. We can fix this with some type families.

Second attempt

{-# LANGUAGE MultiParamTypeClasses, FunctionalDependencies, FlexibleInstances,
             UndecidableInstances, TypeFamilies, ScopedTypeVariables #-}

import Data.Proxy

type family StringFuncReturn a where
  StringFuncReturn (String -> b) = StringFuncReturn b
  StringFuncReturn b = b

class PackArgs a where
  packArgs :: [String] -> a -> Maybe (StringFuncReturn a)

instance (StringFuncReturn a ~ r, PackArgs' a r) => PackArgs a where
  packArgs = packArgs' (Proxy :: Proxy r)

class PackArgs' a b where
  packArgs' :: Proxy b -> [String] -> a -> Maybe b

instance {-# OVERLAPPING #-} PackArgs' y z => PackArgs' (String -> y) z where
  packArgs' _ [] _ = Nothing
  packArgs' p (a:as) f = packArgs' p as (f a)

instance {-# OVERLAPPABLE #-} PackArgs' z z where
  packArgs' _ (_:_) _ = Nothing
  packArgs' _ [] z = Just z

And here it is working in action:

ghci> data User = User String String deriving Show
ghci> data Address1 = Address1 String deriving Show
ghci> packArgs ["chris","str"] User
Just (User "chris" "str")
ghci> packArgs ["chris"] User
Nothing
ghci> packArgs [] User
Nothing
ghci> packArgs ["earth"] Address1
Just (Address1 "earth")

Upvotes: 4

Related Questions