Reputation: 3
Imagine I'm writing a webserver, and I want my config to look like this (took some inspiration from nginx):
listen = "localhost"
set error_page = "fail.html"
vhost "hello" {
set error_page = "oops.html"
path = "/var/www/hello/public_html"
}
Some directives, like Listen
, can only be used in global scope, others are only available in vhost scope (Path
), and some, like Set
, are universal, which means I can't use regular ADTs like this:
data GlobalDirective = Listen Text | VirtualHost Text | Set Text Text
data LocalDirective = Path Text | Set Text Text
So I write this code which seems to reflect my mental model:
class Show a => Directive a
class Directive a => GlobalDirective a
class Directive a => LocalDirective a
data GlobalConfig where
GlobalDirectives :: forall a. GlobalDirective a => [a] -> GlobalConfig
instance Show GlobalConfig where
show (GlobalDirectives xs) = "Config" ++ show xs
data VirtualHost where
VirtualHostDirectives :: forall a. LocalDirective a => Text -> [a] -> VirtualHost
instance Directive VirtualHost
instance GlobalDirective VirtualHost
instance Show VirtualHost where
show (VirtualHostDirectives name xs) = "VirtualHost[" ++ show name ++ "]" ++ show xs
data Listen = Listen Text deriving (Show)
instance Directive Listen
instance GlobalDirective Listen
data Set = Set Text Text deriving (Show)
instance Directive Set
instance GlobalDirective Set
instance LocalDirective Set
data Path = Path Text deriving (Show)
instance Directive Path
instance LocalDirective Path
The problem is that I can't actually construct the config:
> VirtualHostDirectives (Text.pack "hello") [Set (Text.pack "error_page") (Text.pack "oops.html"), Path (Text.pack "/var/www") ]
<interactive>:139:98:
Couldn't match expected type ‘Set’ with actual type ‘Path’
In the expression: Path (Text.pack "/var/www")
In the second argument of ‘VirtualHostDirectives’, namely
‘[Set (Text.pack "error_page") (Text.pack "oops.html"),
Path (Text.pack "/var/www")]’
This looks like a case for an existential quantification hack:
data AnyDirective a = forall a. Directive a => AnyDirective a
instance Directive (AnyDirective a)
instance Show (AnyDirective a) where
show (AnyDirective a) = show a
instance GlobalDirective a => GlobalDirective (AnyDirective a)
instance LocalDirective a => LocalDirective (AnyDirective a)
...but it doesn't work either:
*Main> VirtualHostDirectives (Text.pack "hello") [AnyDirective $ Set (Text.pack "error_page") (Text.pack "oops.html"), AnyDirective $ Path (Text.pack "/var/www") ]
<interactive>:134:1:
No instance for (LocalDirective a0)
arising from a use of ‘VirtualHostDirectives’
The type variable ‘a0’ is ambiguous
Note: there are several potential instances:
instance LocalDirective Path -- Defined at src/Main.hs:167:10
instance LocalDirective Set -- Defined at src/Main.hs:163:10
instance LocalDirective a => LocalDirective (AnyDirective a)
-- Defined at src/Main.hs:142:10
In the expression:
VirtualHostDirectives
(Text.pack "hello")
[AnyDirective
$ Set (Text.pack "error_page") (Text.pack "oops.html"),
AnyDirective $ Path (Text.pack "/var/www")]
In an equation for ‘it’:
it
= VirtualHostDirectives
(Text.pack "hello")
[AnyDirective
$ Set (Text.pack "error_page") (Text.pack "oops.html"),
AnyDirective $ Path (Text.pack "/var/www")]
So, what are my options here?
Upvotes: 0
Views: 115
Reputation: 153257
One possibility would be to have a GADT with a phantom parameter:
{-# LANGUAGE GADTs, DataKinds #-}
import Data.Text
data Scope = Local | Global
data Directive a where
Set :: Text -> Text -> Directive a
Path :: Text -> Directive Local
Listen :: Text -> Directive Global
VirtualHost :: Text -> Directive Global
You can see in ghci that this gives some pretty nice inference:
> :set -XOverloadedStrings
> :t [Set "error_page" "oops.html", Path "/var/www"]
[Set "error_page" "oops.html", Path "/var/www"] :: [Directive 'Local]
Upvotes: 5
Reputation: 92117
You can perfectly well do this with just ordinary ADTs - the only reason you're having trouble is because of the name conflict between the two data constructors:
Set :: Text -> Text -> GlobalDirective
Set :: Text -> Text -> LocalDirective
The simplest solution would be to just give them different names:
data GlobalDirective = Listen Text | VirtualHost Text | GSet Text Text
data LocalDirective = Path Text | LSet Text Text
That might even make the most semantic sense, if global sets are different from local sets in some way. Of course, you can't pass a generic Set
to any function, because it has to know whether it's getting a global or local set. So you can clean this up a bit by defining a reusable Set
that holds two Text
s, and pass that around instead:
data Set = Set Text Text
data GlobalDirective = Listen Text | VirtualHost Text | GSet Set
data LocalDirective = Path Text | LSet Set
whatever :: Set -> Bool
whatever (Set name value) = True
handle :: LocalDirective -> Bool
handle (Path _) = False
handle (LSet s) = whatever s
Given how simple this is to do with ordinary ADTs, I don't think there is any need to bring in fancier stuff like existential types or typeclasses.
Upvotes: 2