|
From: Bjorn B. <bj...@br...> - 2008-03-24 10:57:37
|
Hi Justin,
On Thu, Mar 20, 2008 at 12:06 AM, Justin Bailey <jgb...@gm...> wrote:
> All,
>
> A feature haskelldb is lacking is a way to use SQL functions in
> queries which are not defined by the library. For example, the "trim"
> function is used frequently but can only be included in a haskelldb
> query through the 'literal' combinator or by writing your own function
> which creates the appropriate PrimExpr value.
Yes, that would be nice.
> What I'd like to be able to do is use SQL functions in a type-safe
> way, just like those included in the library. For example, a trim
> function would look like:
>
> trim :: Expr (Maybe a) -> Expr (Maybe String) -- Nulls are still null
Hmm, what if you want to apply this to a NOT NULL value? Should trim
be overloaded? Or maybe there should be a some kind of lifting
function to handle Maybes?
> To use the function in a project is as simple as:
>
> project $ rawNameField << customers ! Customers.name #
> name << trim (customers ! Customers.name)
>
>
> Some functions take multiple arguments. For example. 'rpad', which
> takes a column, a padding length, and an optional padding character.
> Notice not all arguments are expressions:
>
> rpad :: Expr a -> Expr Int -> Maybe String -> Expr (Maybe String)
>
> Sometimes the aggregate expressions provided by haskelldb aren't
> enough. The 'every' function is an aggregate found on postgresql:
>
> every :: Expr Bool -> ExprAggr Bool
>
> The code below allows these functions to be implemented in terms of
> two combinators - func and arg. It is intended to be included in the
> Query module, and only func and arg would exported. Questions I'd like
> answered:
>
> * Is the approach over-engineered? Is there a simpler way?
> * Is the feature useful?
> * Comments on the implementation?
>
> After the code the bodies of the functions above are given.
>
> -- Used to construct type-safe function definitions with
> -- arbitraryly typed exprssions. See func and arg to use these.
> -- The data and instances below are modeled on RecNil/RecCons from
> -- HDBRec.
> data ExprNil = ExprNil
> data ExprCons a b = ExprCons a b
>
> instance ToPrimExprs ExprNil where
> toPrimExprs ~ExprNil = []
>
> instance (ExprC e, ToPrimExprs r) => ToPrimExprs (ExprCons (e a) r) where
> toPrimExprs ~(ExprCons e r) = primExpr e : toPrimExprs r
>
> class (ExprC e) => MakeFunc e r where
> {- | Combinator which can be used to define SQL functions which will
> appear in queries. Each argument for the function is specified by the arg
> combinator. All arguments must be chained together via function composition.
> Examples include:
>
> lower :: Expr a -> Expr (Maybe String)
> lower str = func "lower" $ arg str
>
> The arguments to the function do not have to be Expr if they can
> be converted to Expr:
>
> data DatePart = Day | Century deriving Show
>
> datePart :: DatePart -> Expr (Maybe CalendarTime) -> Expr (Maybe Int)
> datePart date col = func "date_part" $ arg (constant $ show date) . arg col
>
> Aggregate functions can also be defined:
>
> every :: Expr Bool -> ExprAggr Bool
> every col = func "every" $ arg col
>
> Note that type signatures are required on each function defined, as func is
> defined in a typeclass. Also, because of the implementation of aggregates,
> only one argument can be provided.-}
> func :: (ExprC e, ToPrimExprs r) => String -- | The name of the function
> -> (ExprNil -> ExprCons (Expr d) r) -- | The arguments,
> specified via arg combinators joined by composition.
> -> e o -- | The resulting expression.
>
> -- | This instance ensures only one argument can be provided to
> -- an aggregate function.
> instance MakeFunc ExprAggr ExprNil where
> func = funcA
>
> funcA :: String -> (ExprNil -> ExprCons (Expr a) ExprNil) -> ExprAggr o
> funcA name args = ExprAggr (AggrExpr (AggrOther name) (primExpr .
> unExprs $ (args ExprNil)))
> where
> unExprs :: ExprCons (Expr a) ExprNil -> Expr a
> unExprs ~(ExprCons e _) = e
>
> -- | This instance allows any number of expressions to be used as
> -- arguments to a non-aggregate function.
> instance MakeFunc Expr a where
> func = funcE
>
> funcE :: (ToPrimExprs r) => String -> (ExprNil -> ExprCons (Expr e)
> r) -> Expr o
> funcE name args = Expr (FunExpr name (toPrimExprs (args ExprNil)))
>
> -- | Used to specify an individual argument to a SQL function definition. This
> -- combinator must be strung together with function composition. That chain
> -- must be provided to the func combinator.
> arg :: (Expr a) -> (c -> ExprCons (Expr a) c)
> arg expr = ExprCons expr
>
> I modeled the code above after the (#) function and the RecCons/RecNil
> types from the HDBRec module. In this case, a "list" of expressions is
> built through composition with the arg combinator and handed to the
> func combinator. Depending on the result type (Expr or ExprAggr), one
> of the two instances is selected. Since ExprAggr can only take one
> argument, the instance is only defined for (ExprCons x ExprNil). A
> really ugly error will result if more than one argument is defined for
> an aggregrate. Non-aggregates, however, can take any number of
> arguments.
Hmm, I get the feeling that this could be implemented in a simpler way.
If we ignore the one-argument restriction on aggregates, couldn't you
use the same trick as Text.Printf or the Remote class in HaXR (see
http://darcs.haskell.org/haxr/Network/XmlRpc/Client.hs). The aggregate
restriction could be handled by having a separate 'func' for
aggregates, with a simpler type.
Also, why are ExprNil and ExprCons needed? Couldn't func produce a
PrimExpr internally?
> The implementation of the motivating functions is then:
>
> trim str = func "trim" $ arg str
>
> rpad str len (Just char) = func "rpad" $ arg str . arg (constant
> len) . arg (constant char)
> rpad str len Nothing = func "rpad" $ arg str . arg (constant len)
>
> every col = func "every" $ arg col
>
> Thanks in advance to any responders!
>
> Justin
With the solution I refer to above, you should be able to write them
as (not tested of course):
trim str = func "trim" str
rpad str len (Just char) = func "rpad" str (constant len) (constant char)
rpad str len Nothing = func "rpad" str (constant len)
every col = funcAggr "every" col
/Bjorn
|