snaplet-mongoDB / src / Snap / Snaplet / MongoDB / Template.hs

root df0ea16 
















































































































































































































































































































































































































































































































  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
{-# LANGUAGE TemplateHaskell, QuasiQuotes #-}
-- |
-- Module      : Snap.Snaplet.MongoDB.Template
-- Description : Template Haskell functions for creating MongoDB entities.
-- Copyright   : (C) 2011 Massive Tactical Limited
-- License     : BSD3
--
-- Maintainer  : Blake Rain <blake.rain@massivetactical.com>
-- Stability   : Provisional
-- Portability : Unknown
--
-- Various Template Haskell functions for creating MongoDB entities.
--

module Snap.Snaplet.MongoDB.Template
       ( asMongoEntity
       , useDefaults
       , setCollectionName
       , forConstructor
       , ConstructorOp
       , setConstructorName
       , renameFields
       , assocFieldNames
       , indexedFieldName
       , setFieldReadOnly
       , asMongoValue
       , encodedViaShow
       ) where

import           Control.Applicative
import           Control.Monad.Error
import           Control.Monad.State
import           Control.Monad.Writer
import qualified Data.Bson as BSON
import           Data.Char (toUpper, toLower)
import           Data.List (find)
import           Data.Maybe (catMaybes)
import           Data.Text (Text)
import qualified Data.Text as T
import           Language.Haskell.TH.Syntax hiding (lift)
import           Snap.Snaplet.MongoDB.MongoValue
import           Snap.Snaplet.MongoDB.MongoEntity


-- Represents a field declaration from the type we are to store.
data FieldDecl = -- A field in a record data type constructor.
                 FieldDecl { fieldName       :: Name    -- The actual name of the field.
                           , fieldSimpleName :: String  -- The simple name of the field, as stored in the document (can be changed).
                           , fieldType       :: Type
                           , fieldReadOnly   :: Bool    -- If 'True', the field will not be output by the 'toDocument' function.
                           }
                 -- Represents a field in a 'NormalC' data type constructor.
               | SimpleFieldDecl { simpleFieldName :: String
                                 , simpleFieldType :: Type }

-- Represents a constructor to a data type. We blend both 'NormalC' and 'RecordC' data type constructors into this type.
data Constructor = Constructor { constrName       :: Name         -- The actual name of the constructor.
                               , constrSimpleName :: String       -- The simple name of the constructor, as stored in the _type field in the document (can be changed).
                               , constrBody       :: [FieldDecl]
                               }

-- The environment in which our template builder lives. This is actually used as _state_, but whatever.
data TemplateEnv = TemplateEnv { envTypeName       :: Name            -- The name of the type we are generating a MongoEntity instance for.
                               , envSimpleTypeName :: String          -- The simple name of the type.
                               , envCollectionName :: String          -- The name of the collection to which we are to store this type.
                               , envEncodedViaShow :: Bool            -- Is the 'MongoValue' instance encoded via Show/Read rather than toDocument/fromDocument?
                               , envConstructors   :: [Constructor]
                               }

type EndoFunctor a = a -> a
type TemplateGen = StateT TemplateEnv (WriterT (EndoFunctor [Dec]) Q)


-- | This function is used to indicate to 'asMongoEntity' and 'asMongoValue' that the default behaviour is to be used.
useDefaults :: TemplateGen ()
useDefaults = return ()

-- | This function generates an instance of the 'MongoEntity' and 'MongoValue' type classes for the specified type.
asMongoEntity :: Name -> TemplateGen () -> Q [Dec]
asMongoEntity typeName actions =
  runTemplateGen typeName (actions >> genEntityInstance >> genValueInstance)

-- | This function generates an instance of the 'MongoValue' type class for the given type. If the type is to be encoded
-- via 'show' and 'read', then a 'MongoEntity' instance is /not/ created; otherwise one will be.
asMongoValue :: Name -> TemplateGen () -> Q [Dec]
asMongoValue typeName actions = do
  runTemplateGen typeName $ do
    actions
    viaShow <- gets envEncodedViaShow
    if viaShow then genValueViaShowInstance else (genEntityInstance >> genValueInstance)


-- | Set the name of the collection to which the type is to be stored. By default, the name of the collection is the
-- same as the name of the type.
setCollectionName :: String -> TemplateGen ()
setCollectionName newName =
  modify (\s -> s { envCollectionName = newName })

-- | Sets whether the 'MongoValue' instance for the type should be encoded via 'read' and 'show' rather than
-- 'fromDocument' and 'toDocument'.
encodedViaShow :: TemplateGen ()
encodedViaShow =
  modify (\s -> s { envEncodedViaShow = True })

-- | Operations over constructors live in the 'ConstructorOp' monad.
type ConstructorOp = StateT Constructor TemplateGen

-- | For @foConstructor name ops@, perform @ops@ for the constructor matching @name@. This allows us to change the name
-- of the constructor stored in the @_type@ field, and alter the behaviour of individual fields.
forConstructor :: Name -> ConstructorOp () -> TemplateGen ()
forConstructor name f = do
  constrs <- dConstr name f =<< gets envConstructors
  modify (\s -> s { envConstructors = constrs })
  where
    dConstr _    _       [] = return []
    dConstr name f (x : xs)
      | constrName x == name = do
        (_, x') <- runStateT f x
        return (x' : xs)
      | otherwise           = do
        xs' <- dConstr name f xs
        return (x : xs')

-- | Set the name stored in the @_type@ field of the document for the current constructor.
setConstructorName :: String -> ConstructorOp ()
setConstructorName newName = do
  modify (\c -> c { constrSimpleName = newName })

-- | Set the field with the specified name to read only. This will mean that the 'toDocument' function will not output
-- this field. This means that the field's value will not be written to the document.
setFieldReadOnly :: Name -> ConstructorOp ()
setFieldReadOnly name =
  modify (\c -> c { constrBody = dField name (constrBody c) })
  where
    dField _          [] = []
    dField name (f : fs)
      | fieldName f == name =
        f { fieldReadOnly = True } : fs
      | otherwise =
        f : dField name fs

-- | Change the names of a number of fields. The default behaviour is for the fields in the document to have the same
-- name as the fields in the record type constructor.
assocFieldNames :: [(Name, String)] -> ConstructorOp ()
assocFieldNames assocs =
  modify (\c -> c { constrBody = dField (constrBody c) })
  where
    dField               [] = []
    dField (field : fields) =
      case field of
        FieldDecl name _ _ _ ->
          (maybe field (\newName -> field { fieldSimpleName = newName }) $ lookup name assocs) : dField fields
        SimpleFieldDecl name _ ->
          let name' = mkName name in (maybe field (\newName -> field { simpleFieldName = newName }) $ lookup name' assocs) : dField fields

-- | Renames fields in the order they are found in the type constructor.
renameFields :: [String] -> ConstructorOp ()
renameFields newNames =
  modify (\c -> c { constrBody = zipWith renameField newNames (constrBody c) })
  where
    renameField newName (FieldDecl fn _ ft ro) = FieldDecl fn newName ft ro
    renameField newName (SimpleFieldDecl _ ft) = SimpleFieldDecl newName ft


indexedFieldName :: Int -> Name
indexedFieldName = mkName . ("field" ++) . show


runTemplateGen :: Name -> TemplateGen () -> Q [Dec]
runTemplateGen typeName actions = do
  (simpleName, constructors) <- getSimpleNameAndConstrs typeName
  
  let env = TemplateEnv { envTypeName       = typeName
                        , envSimpleTypeName = simpleName
                        , envCollectionName = simpleName
                        , envEncodedViaShow = False
                        , envConstructors   = buildConstructors constructors
                        }
  (_, decls) <- runWriterT (runStateT actions env)
  return $ decls []


inner :: TemplateGen a -> TemplateGen (a, [Dec])
inner action = do
  state <- get
  ((result, newState), decls) <- lift . lift $ runWriterT (runStateT action state)
  put newState
  return (result, decls [])


getSimpleNameAndConstrs :: Name -> Q (String, [Con])
getSimpleNameAndConstrs typeName = do
  typeInfo <- reify typeName
  case typeInfo of
    TyConI tyCon ->
      case tyCon of
        DataD    _ n _ c _ -> return (dropPrefix $ show n,  c )
        NewtypeD _ n _ c _ -> return (dropPrefix $ show n, [c])
        _                  -> error $ "No able to handle `" ++ show typeName ++ "'; not a data or newtype"
    _ -> error $ "Not able to handle `" ++ show typeName ++ "'; not a type constructor"


buildConstructors :: [Con] -> [Constructor]
buildConstructors =
  map buildConstructor
  where
    buildConstructor :: Con -> Constructor
    buildConstructor (NormalC name types) =
      Constructor { constrName       = name
                  , constrSimpleName = dropPrefix $ show name
                  , constrBody       = map (\(n, (_, t)) -> SimpleFieldDecl { simpleFieldName = "field" ++ show n, simpleFieldType = t }) $ zip [1 ..] types
                  }
    buildConstructor (RecC name fields) =
      Constructor { constrName       = name
                  , constrSimpleName = dropPrefix $ show name
                  , constrBody       = map (\(n, _, t) ->
                                             let (r, n') = if ((dropPrefix . show $ n) == ((uncapitalize . dropPrefix . show $ name) ++ "Id"))
                                                              then (True, "_id")
                                                              else (False, dropPrefix $ show n)
                                             in FieldDecl { fieldName       = n
                                                          , fieldSimpleName = n'
                                                          , fieldType       = t
                                                          , fieldReadOnly   = r
                                                          })
                                           fields
                  }
    buildConstructor _ = error "Cannot build constructor for non-record or non-normal data type constructor"


-- Emits a declaration in the underlying writer monad.
emitDecl :: Dec -> TemplateGen ()
emitDecl = lift . tell . (:)

-- Generate the key newtype and the 'fromKey' and 'toKey' functions for the specified type. For a given type MyType, we
-- generate a newtype with the following definition:
--
--     newtype Key MyType = MyTypeId { unMyTypeId :: ObjectId }
--                          deriving (Show)
--
-- The two functions 'toKey' and 'fromKey' are simply aliases for constructing and deconstructing this type:
--
--     toKey = MyTypeId
--     fromKey = unMyTypeId
--
genKeyDecls :: TemplateGen ()
genKeyDecls = do
  name  <- gets envTypeName
  name' <- gets envSimpleTypeName
  emitDecl $ NewtypeInstD [] ''Key [ConT name]
                          (RecC (mkName $ name' ++ "Id")
                                [ (mkName $ "un" ++ name' ++ "Id", NotStrict, ConT ''ObjectId) ])
                          [ ''Eq ]
  emitDecl $ FunD (mkName "toKey")
                  [ Clause [] (NormalB . ConE . mkName $ name' ++ "Id") [] ]
  emitDecl $ FunD (mkName "fromKey")
                  [ Clause [] (NormalB . VarE . mkName $ "un" ++ name' ++ "Id") []]


genDocumentDecls :: TemplateGen ()
genDocumentDecls = do
  name  <- gets envTypeName
  name' <- gets envSimpleTypeName
  emitDecl $ NewtypeInstD [] ''Document [ConT name]
                          (RecC (mkName $ name' ++ "Document")
                                [ (mkName $ "un" ++ name' ++ "Document", NotStrict, ConT ''BSON.Document) ])
                          []
  emitDecl $ FunD (mkName "toDocument")
                  [ Clause [] (NormalB . ConE . mkName $ name' ++ "Document") [] ]
  emitDecl $ FunD (mkName "fromDocument")
                  [ Clause [] (NormalB . VarE . mkName $ "un" ++ name' ++ "Document") [] ]


genCollNameDecl :: TemplateGen ()
genCollNameDecl = do
  cName <- gets envCollectionName
  emitDecl $ FunD (mkName "collectionName")
                  [ Clause [WildP] (NormalB $ LitE $ StringL cName) [] ]


-- This function generates the filter declaration. The filter type instance has the following form:
--
--     data Filter MyType = MyFieldA Int | MyFieldB String | ...
--
-- Where each field in MyType has a constructor in the (Filter MyType) data type.
genFilterDecl :: TemplateGen ()
genFilterDecl = do
  name    <- gets envTypeName
  constrs <- gets envConstructors
  let cons = catMaybes $ concatMap (\c -> map (buildFieldCon (dropPrefix . show $ constrName c)) (constrBody c)) constrs
  emitDecl $ DataInstD [] ''Filter [ConT name]
                       (map fst cons)
                       []
  let clauses = map (\((NormalC name _), fName) ->
                      Clause [ConP name [WildP]] (NormalB . LitE . StringL . dropPrefix . show $ fName) []) cons
  if not . null $ clauses
    then emitDecl $ FunD (mkName "filterFieldName") clauses
    else emitDecl $ FunD (mkName "filterFieldName") [Clause [WildP] (NormalB ((VarE 'error) `AppE` (LitE $ StringL "no filters can be defined"))) []]
  where
    buildFieldCon :: String -> FieldDecl -> Maybe (Con, Name)
    buildFieldCon constrName (FieldDecl fieldName simpleName fType _) =
      if ((dropPrefix . show $ fieldName) == (uncapitalize constrName ++ "Id")) ||
         (simpleName == "_id") 
         then Nothing
         else Just (NormalC (mkName . capitalize . dropPrefix . show $ fieldName) [(NotStrict, fType)], mkName simpleName)
    buildFieldCon _ (SimpleFieldDecl _ _) =
      Nothing

                                                                               

-- This function generates the 'encodeDocument' function for the 'MongoEntity' instance. The 'encodeDocument' function
-- generates a Document from an instance of the type. For example, given the type:
--
--     data A = A { fieldA :: Int
--                , fieldB :: String
--                }
--            | B { fieldC :: String
--                , fieldD :: Int
--                }
--
-- A document will have one of the following forms:
--
--     { _type: "A", fieldA: 123, fieldB: "hello" }
--     { _type: "B", fieldA: "world", fieldB: 456 }
--
-- The Haskell code to generate this will be equivalent to:
--
--     encodeDocument (A field1 field2) = toDocument [ "_type" := toValue "A", "fieldA" := toValue field1, "fieldB" := toValue field2 ]
--     encodeDocument (B field1 field2) = toDocument [ "_type" := toValue "B", "fieldC" := toValue field1, "fieldD" := toValue field2 ]
--
--
-- Note: The extra field '_type' is stored in the document such that we know which constructor to use when we load the
-- document back into Haskell.
--
-- Note: A field will not be written to the document if it has been set 'readOnly'.
--
-- Note: A field will not be written to the document if it matches as the ID of the document. A field is recognised as
-- the ID of the document if it's name matches the constructor of the type, followed by "Id". For example, for a data
-- type constructor 'MyType', a field with the name 'myTypeId' will be assumed to be the ID field.
--
genEncodeDocument :: TemplateGen ()
genEncodeDocument = do
  constrs <- gets envConstructors
  emitDecl $ FunD (mkName "encodeDocument") $ map buildClause constrs
  where
    buildClause :: Constructor -> Clause
    buildClause constr =
--      let patNames = map (mkName . ("field" ++) . show . fst) $ zip [1 ..] (constrBody constr)
      let fn (FieldDecl   _ n _ _) = n
          fn (SimpleFieldDecl n _) = n
          patNames  = map (mkName . ('_' :) . fn) (constrBody constr)
          setType   = InfixE (Just . LitE $ StringL "_type")
                             (ConE '(:=))
                             (Just ((VarE 'toValue) `AppE` ((LitE $ StringL (constrSimpleName constr)) `SigE` (ConT ''Text))))
          setFields = catMaybes (map (genField (dropPrefix . show $ constrName constr)) (zip patNames (constrBody constr)))
      in Clause [ConP (constrName constr) (map VarP patNames)]
                (NormalB $ ((VarE 'toDocument) `AppE` (ListE (setType : setFields))))
                []
    
    genField :: String -> (Name, FieldDecl) -> Maybe Exp
    genField constrName (patName, SimpleFieldDecl fieldName _) =
      Just (InfixE (Just . LitE . StringL $ fieldName) (ConE '(:=)) (Just ((VarE 'toValue) `AppE` (VarE patName))))
    genField constrName (patName, FieldDecl fieldName simpleName _ readOnly) =
      if readOnly ||
         ((dropPrefix . show $ fieldName) == (uncapitalize constrName ++ "Id")) ||
         (simpleName == "_id")
         then Nothing
         else Just (InfixE (Just . LitE . StringL $ simpleName) (ConE '(:=)) (Just ((VarE 'toValue) `AppE` (VarE patName))))

{-
decodeDocument doc' = do
  let doc = fromDocument doc'
  typ <- lookupThrow "_type" doc
  case typ of
    "A"   -> A <$> lookupThrow "fieldA" doc <*> lookupThrow "fieldB" doc
    other -> throwError $ "Unknown type constructor '" ++ other ++ "' found in collection for data type '" ++ "A"
-}

genDecodeDocument :: TemplateGen ()
genDecodeDocument = do
  name    <- gets envTypeName
  cName   <- gets envCollectionName
  constrs <- gets envConstructors
  let doc' = mkName "doc'"
  emitDecl $ FunD (mkName "decodeDocument") $
                  [ Clause [VarP doc'] (NormalB $ buildBody name cName doc' constrs) [] ]
  where
    buildBody :: Name -> String -> Name -> [Constructor] -> Exp
    buildBody name cName doc' constrs =
      let typ   = mkName "typ"
          other = mkName "other"
          doc   = mkName "doc"
      in DoE [ LetS [ ValD (VarP doc) (NormalB ((VarE 'fromDocument) `AppE` (VarE doc'))) [] ]
             , BindS (VarP typ) (((VarE 'lookupThrow) `AppE` (LitE $ StringL "_type")) `AppE` (VarE doc))
             , NoBindS $ CaseE (VarE typ) (map (buildMatch doc) constrs ++ [ Match (VarP other)
                                                                                   (NormalB $ ((VarE 'throwError) `AppE`
                                                                                               ((VarE 'concat) `AppE`
                                                                                                ListE [ LitE $ StringL "Unknown type constructor '"
                                                                                                      , ((VarE 'T.unpack) `AppE` (VarE other))
                                                                                                      , LitE $ StringL "' found in collection for data type '"
                                                                                                      , LitE $ StringL (dropPrefix $ show name)
                                                                                                      , LitE $ StringL "' ("
                                                                                                      , LitE $ StringL cName 
                                                                                                      , LitE $ StringL ")"]))) []]) ]

    buildMatch :: Name -> Constructor -> Match
    buildMatch doc constr =
      let body = if null (constrBody constr)
                    then ((VarE 'return) `AppE` (ConE (constrName constr)))
                    else foldl (genFieldApp doc)
                               (InfixE (Just (ConE $ constrName constr))
                                       (VarE '(<$>))
                                       (Just (genFieldLookup doc $ head (constrBody constr))))
                               (tail (constrBody constr))
      in Match (LitP $ StringL $ constrSimpleName constr) (NormalB $ body) []

    genFieldApp :: Name -> Exp -> FieldDecl -> Exp
    genFieldApp doc lhs fieldDecl =
      InfixE (Just lhs)
             (VarE '(<*>))
             (Just $ genFieldLookup doc fieldDecl)

    genFieldLookup :: Name -> FieldDecl -> Exp
    genFieldLookup doc (SimpleFieldDecl simpleName _) =
      ((VarE 'lookupThrow) `AppE` (LitE $ StringL simpleName)) `AppE` (VarE doc)
    genFieldLookup doc (FieldDecl _ simpleName _ _) =
      ((VarE 'lookupThrow) `AppE` (LitE $ StringL simpleName)) `AppE` (VarE doc)


genEntityInstance :: TemplateGen ()
genEntityInstance = do
  name          <- gets envTypeName
  (_, decls)    <- inner $ do
                    genKeyDecls
                    genDocumentDecls
                    genCollNameDecl
                    genFilterDecl
                    genEncodeDocument
                    genDecodeDocument
  emitDecl $ InstanceD [] (ConT ''MongoEntity `AppT` ConT name) decls


genValueViaShowInstance :: TemplateGen ()
genValueViaShowInstance = do
  name <- gets envTypeName
  emitDecl $ InstanceD [] (ConT ''MongoValue `AppT` ConT name)
                          [ FunD (mkName "toValue")
                              [ Clause []
                                (NormalB (InfixE (Just (VarE 'toValue)) (VarE '(.)) (Just $ InfixE (Just (VarE 'T.pack)) (VarE '(.)) (Just (VarE 'show)))))
                                []
                              ]
                          , FunD (mkName "fromValue")
                              [ Clause [VarP (mkName "v")]
                                (NormalB (InfixE (Just (InfixE (Just (VarE 'return)) (VarE '(.))
                                                                                      (Just $ InfixE (Just (VarE 'read)) (VarE '(.)) (Just (VarE 'T.unpack)))))
                                                 (VarE '(=<<))
                                                 (Just (AppE (VarE 'fromValue) (VarE (mkName "v"))))))
                                []
                              ]
                          ]
  

genValueInstance :: TemplateGen ()
genValueInstance = do
  name   <- gets envTypeName
  let x   = InfixE (Just $ VarE 'toValue) (VarE '(.)) (Just $ InfixE (Just $ VarE 'fromDocument) (VarE '(.)) (Just $ VarE 'encodeDocument))
  let y d = InfixE (Just $ InfixE (Just $ VarE 'decodeDocument) (VarE '(.)) (Just $ VarE 'toDocument)) (VarE '($)) (Just d)
  let doc = mkName "doc"
      val = mkName "val"
  emitDecl $ InstanceD [] (ConT ''MongoValue `AppT` ConT name)
                       [ FunD (mkName "toValue")
                              [ Clause [] (NormalB x) [] ]
                       , FunD (mkName "fromValue")
                              [ --Clause [ConP 'Doc [VarP doc]] (NormalB ((VarE 'fromDocument) `AppE` (VarE doc))) []
                                Clause [ConP 'Doc [VarP doc]] (NormalB $ y (VarE doc)) []
                              , Clause [VarP val] (NormalB (((VarE 'expected) `AppE` (LitE $ StringL "Document")) `AppE` (VarE val))) []
                              ]
                       ]
    



dropPrefix :: String -> String
dropPrefix = reverse . takeWhile (/= '.') . reverse

capitalize :: String -> String
capitalize []       = []
capitalize (c : cs) = toUpper c : cs

uncapitalize :: String -> String
uncapitalize []       = []
uncapitalize (c : cs) = toLower c : cs


-- Local Variables:
-- mode                  : Haskell
-- fill-column           : 120
-- default-justification : left
-- End:
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.