Haskell: HList and optional parameters
Asked Answered
P

1

6

I've been trying to use HList to create records.

I've been using the operators defined in HList-GHCSyntax.

It so far works quite nicely, allowing me to write things like this:

myRecord = 
  (param1 .=. "param1value") .*. 
  (param2 .=. "param2value") .*. 
  emptyRecord

This allows me to do the following:

myRecord .!. param1

and the following:

myRecord .!. param3

throws a compile error as expected. This works great if param3 is required, as I get compile time parameter checking.

But I also want to deal with the case where param3 is optional. How can I do this?


Edit: The following seems to work (Empty is an empty type):

getOptional r l = (hLeftUnion r ((l .=. Empty) .*. emptyRecord)) .!. l

But I don't really know how to check for Empty in calling code.

Polyptych answered 22/8, 2012 at 1:30 Comment(0)
E
5

The problem with defining getOptional is determining the result type. If one tries:

class GetOptional r l v | r l -> v where
  getOptional :: l -> Record r -> Maybe vs

or

class GetOptional r l v | r l -> v where
  getOptional :: l -> Record r -> Maybe v

Then v can be determined by looking up l in r when present, but if l is not in r then from where should v come? Pick () or Empty? Leaving off the functional dependency makes the user supply a type annotation somewhere.

Perhaps a better way is to provide a default value (like fromMaybe):

class GetOptional r l v where
  getOptional :: l -> v -> Record r -> v

A more complicated version might supply a function to consume an existing value (v->w) and a default value w.

This works for me:

{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE UndecidableInstances #-}
import Data.HList.FakePrelude(HEq,HTrue,HFalse)
import Data.HList.HListPrelude(HNil(HNil),HCons(HCons))
import Data.HList.GhcSyntax((.=.),(.*.))
import Data.HList.Record(Record(Record),LVPair(LVPair),emptyRecord)

class GetOptional l r v where
  getOptional :: l -> v -> Record r -> v

instance GetOptional l HNil v where
  getOptional _ v _ = v

instance ( HEq l l' b
         , GetOptional' b l (HCons (LVPair l' v') r) v
         )
         => GetOptional l (HCons (LVPair l' v') r) v where
  getOptional l v (Record r) = getOptional' (undefined :: b) l v r

class GetOptional' b l r v where
  getOptional' :: b -> l -> v -> r -> v

instance GetOptional' HTrue l (HCons (LVPair l v) r) v where
  getOptional' _ _ _ (HCons (LVPair v) _) = v

instance ( GetOptional l r v
         )
         => GetOptional' HFalse l (HCons (LVPair l' v') r) v where
  getOptional' _ l v (HCons _ r) = getOptional l v (Record r)


data L1 = L1
data L2 = L2

e = emptyRecord
f = L1 .=. True .*. emptyRecord

-- test1 :: Bool
test1 = getOptional L1 False f
-- test2 :: Bool
test2 = getOptional L1 False e
-- test3 :: ()
test3 = getOptional L2 () f
-- test4 gives a type error:
-- test4 = getOptional L1 () f

I also include below second implementation of this using "higher level" HList predicates. This removes the GetOptional type class and makes getOptional a simple function:

{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE UndecidableInstances #-}
import Data.HList.FakePrelude(HFalse,HTrue)
import Data.HList.HListPrelude(HMember,hMember)
import Data.HList.GhcSyntax((.=.),(.*.))
import Data.HList.Record(RecordLabels,Record,HasField(hLookupByLabel),recordLabels,emptyRecord)

-- This type is inferred properly
-- getOptional :: ( RecordLabels r ls
--                , HMember l ls b
--                , GetOptional' b l r v )
--               =>  l -> v -> Record r -> v
getOptional l v rec = getOptional' (hMember l (recordLabels rec)) l v rec

class GetOptional' b l r v where
  getOptional' :: b -> l -> v -> Record r -> v

instance GetOptional' HFalse l rec v where
  getOptional' _ _ v _ = v

instance ( HasField l r v )
         => GetOptional' HTrue l r v where
  getOptional' _ l _ r = hLookupByLabel l r


data L1 = L1
data L2 = L2

e = emptyRecord
f = L1 .=. True .*. emptyRecord

-- test1 :: Bool
test1 = getOptional L1 False f
-- test2 :: Bool
test2 = getOptional L1 False e
-- test3 :: ()
test3 = getOptional L2 () f
-- test4 gives a type error:
-- test4 = getOptional L1 () f

EDIT: Here is the Maybe version which needs type annotations for all the Nothing answers:

{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE UndecidableInstances #-}
import Data.HList.FakePrelude(HFalse,HTrue)
import Data.HList.HListPrelude(HMember,hMember)
import Data.HList.GhcSyntax((.=.),(.*.))
import Data.HList.Record(RecordLabels,Record,HasField(hLookupByLabel),recordLabels,emptyRecord)
import Data.HList.TypeCastGeneric1
import Data.HList.TypeEqGeneric1
import Data.HList.Label5

-- getOptional :: ( RecordLabels r ls
--                , HMember l ls b
--                , GetOptional' b l r v )
--               =>  l -> Record r -> Maybe v
getOptional l rec = getOptional' (hMember l (recordLabels rec)) l rec

class GetOptional' b l r v where
  getOptional' :: b -> l -> Record r -> Maybe v

instance GetOptional' HFalse l rec v where
  getOptional' _ _ _ = Nothing

instance ( HasField l r v )
         => GetOptional' HTrue l r v where
  getOptional' _ l r = Just (hLookupByLabel l r)


data L1 = L1
data L2 = L2

e = emptyRecord
f = L1 .=. True .*. emptyRecord

test1 = getOptional L1 f
test2 = getOptional L1 e
test3 = getOptional L2 f
-- test4 :: Maybe () -- this would be a type error
-- test4 = getOptional L1 f

main = print ( test1 -- inferred becuase it is Just {}
             , test2 :: Maybe () -- must specify for Nothing
             , test3 :: Maybe () -- must specify for Nothing
             )
Extrasystole answered 22/8, 2012 at 12:43 Comment(6)
Thanks Chris, this looks good! One follow up question, could you add another response that allows one to make the default value Nothing, and the return value Just v? Most of the parameters don't have defaults, if they're empty, they're simply empty (which is different to blank).Polyptych
Replacing (default :: v) with (Nothing :: forall a . Maybe a) is like your Empty before. It does not help with type inference.Extrasystole
I have added the Maybe version, with the type annotations to make it work.Extrasystole
I know I'm asking a lot here, but would you mind giving an example, similar thing but using the Records package here: hackage.haskell.org/package/records-0.1.1.6 . I'll create a bounty and reward it to you, as all this has been really helpful for me (and is hopefully helpful to others).Polyptych
There is an example: e, f, and test1,2,3 at the end of my code sample.Extrasystole
Chris: Is e, f using the Records package, not HList?Polyptych

© 2022 - 2024 — McMap. All rights reserved.