2

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"
Chris Stryczynski
  • 30,145
  • 48
  • 175
  • 286
  • 1
    Why do you need to abstract over the constructor? How are you going to use the return value of `f` if you don't know its type ahead of time? – chepner Jan 01 '18 at 16:11
  • I would create a typeclass where `User` would have a instance relating to this. So `save (User "chris" "str")` would work as an example. – Chris Stryczynski Jan 01 '18 at 16:20
  • 1
    This seems like a poor design. Rather than try to create one function which does all of the work, you should create several smaller, simpler functions which each deal with a different entity type. – 4castle Jan 01 '18 at 17:21

1 Answers1

4

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")
Alec
  • 31,829
  • 7
  • 67
  • 114