elmental-0.1.0.0: src/Elmental.hs
{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE DefaultSignatures #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE OverloadedRecordDot #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeInType #-}
{-# LANGUAGE UndecidableInstances #-}
{-# LANGUAGE NoFieldSelectors #-}
module Elmental (
ElmDeclarable (..),
HasElmStructure,
ElmMapping (..),
HasSymbolInfo,
ElmKind,
defaultMapping,
getElmStructure,
getTypeName,
getModuleName,
getMapping,
setModule,
module Elmental.ElmStructure,
) where
import Data.Kind (Type)
import Data.Proxy
import Data.Text (Text)
import Data.Text qualified as Text
import Elmental.ElmStructure
import GHC.Generics qualified as GHC
import GHC.TypeLits
import Generics.Kind
type family KindOf (x :: k) :: Type where
KindOf (_ :: k) = k
{- | Class mapping a Haskell type constructor @x :: k@ to an Elm type constructor.
You can define instances for this class for any Haskell data / newtype constructor,
be it unapplied, partially applied or fully applied, provided that its kind is
not Elm-compatible (i.e., not higher-kinded).
For example:
@
data SomeHKT f a = SomeHKT (f a)
instance ElmDeclarable ((Type -> Type) -> Type) SomeHKT -- Not OK: SomeHKT is higher-kinded.
instance ElmDeclarable (Type -> Type) (SomeHKT Maybe) -- OK
instance ElmDeclarable Type (SomeHKT Maybe Int) -- OK
instance ElmDeclarable [] -- OK
instance ElmDeclarable [Char] -- OK
@
-}
class (ElmKind (KindOf x)) => ElmDeclarable x where
-- | Elm mapping information.
--
-- Contains the name / location of the type and its encoder / decoder.
-- Can be overridden.
--
-- Example:
--
-- @
-- instance ElmDeclarable Type [Char] where
-- mapTo = ElmMapping
-- { typeName = "String"
-- , moduleName = Nothing
-- , encoderLocation = Just $ SymbolLocation
-- { symbolName = "string"
-- , moduleName = "Json.Encode"
-- }
-- , decoderLocation = Just $ SymbolLocation
-- { symbolName = "string"
-- , moduleName = "Json.Decode"
-- }
-- , args = []
-- }
-- @
mapTo :: ElmMapping
default mapTo :: (HasSymbolInfo x) => ElmMapping
mapTo = defaultMapping @x
-- | Internal function. You should not have to define this method yourself.
mkTyRef :: PList (NParams (KindOf x)) TyRef -> TyRef
default mkTyRef :: PList (NParams (KindOf x)) TyRef -> TyRef
mkTyRef pList = TyRef (TyMapping (mapTo @x)) (pListToList pList)
{- | Instance for applied type constructors.
Necessary to traverse the list of type constructors down to the root when constructing
type references to applied type constructors.
-}
instance
{-# OVERLAPPABLE #-}
forall k (t :: Type) (x :: Type -> k).
( ElmDeclarable t
, ElmDeclarable (x :: Type -> k)
) =>
ElmDeclarable (x t)
where
mapTo =
let tMapping = mapTo @t
xMapping = mapTo @x
in xMapping{args = xMapping.args <> [tMapping]}
mkTyRef remainingParams = mkTyRef @x ((mkTyRef @t PNil) `PCons` remainingParams)
type HasSymbolInfo x =
( KnownSymbol (GetTypeNameG (RepK x))
, KnownSymbol (GetModuleNameG (RepK x))
)
defaultMapping :: forall x. (HasSymbolInfo x) => ElmMapping
defaultMapping =
ElmMapping
{ typeName = tName
, moduleName = Just mName
, encoderLocation =
Just $
SymbolLocation
{ symbolName = "encode" <> tName
, symbolModuleName = mName
}
, decoderLocation =
Just $
SymbolLocation
{ symbolName = "decode" <> tName
, symbolModuleName = mName
}
, args = []
, isTypeAlias = False
, urlPiece = Nothing
, queryParam = Nothing
}
where
tName = symbolToText @(GetTypeNameG (RepK x))
mName = symbolToText @(GetModuleNameG (RepK x))
{- | Overrides / sets the module name everywhere in a mapping.
Often useful in conjunction wit @defaultMapping@.
-}
setModule :: Text -> ElmMapping -> ElmMapping
setModule moduleName mapping =
mapping
{ moduleName = Just moduleName
, decoderLocation =
( \l ->
l
{ symbolModuleName = moduleName
}
)
<$> mapping.decoderLocation
, encoderLocation =
( \l ->
l
{ symbolModuleName = moduleName
}
)
<$> mapping.encoderLocation
}
-- Type metadata utilities
type family GetModuleNameG x where
GetModuleNameG (M1 _d ('GHC.MetaData _tyConName moduleName _pkg _isNewtype) _sop) = moduleName
type family GetTypeNameG x where
GetTypeNameG (M1 _d ('GHC.MetaData tyConName _moduleName _pkg _isNewtype) _sop) = tyConName
symbolToText :: forall sym. (KnownSymbol sym) => Text
symbolToText = Text.pack $ symbolVal (Proxy @sym)
getMapping :: forall x. (ElmDeclarable x) => ElmMapping
getMapping = mapTo @x
getTypeName :: forall x. (ElmDeclarable x) => Text
getTypeName = (getMapping @x).typeName
getModuleName :: forall x. (ElmDeclarable x) => Maybe Text
getModuleName = (getMapping @x).moduleName
-- Usual Peano numbers / length-indexed lists stuff.
data PNat = Z | S PNat
type family PNatToNat (n :: PNat) :: Natural where
PNatToNat Z = 0
PNatToNat (S n) = 1 + PNatToNat n
data PList (n :: PNat) a where
PNil :: PList Z a
PCons :: a -> PList n a -> PList (S n) a
pListToList :: PList n a -> [a]
pListToList PNil = []
pListToList (a `PCons` as) = a : (pListToList as)
-- | Constraint establishing that a kind is valid in Elm.
type ElmKind k = ElmKindB k ~ True
type family ElmKindB k :: Bool where
ElmKindB Type = True
ElmKindB (Type -> k) = ElmKindB k
-- Compute the number of type parameters of a type constuctor.
type family NParams k :: PNat where
NParams Type = Z
NParams (Type -> k) = S (NParams k)
-- This shouldn't have to be a class as it only has a single instance,
-- but it seems to be the only way to expose @HasElmStructure@ as a simple
-- constraint.
class (repK ~ RepK x) => HasElmStructure' k (x :: k) repK where
getElmStructure' :: DatatypeStructure x
instance
( ElmDeclarable x
, RepK x ~ M1 GHC.D ('GHC.MetaData tName mName pkg isNewtype) sop
, GElmSum sop
, KnownNat (PNatToNat (NParams (KindOf x)))
) =>
HasElmStructure' k x (M1 GHC.D ('GHC.MetaData tName mName pkg isNewtype) sop)
where
getElmStructure' =
DatatypeStructure
{ mapping = getMapping @x
, nParams = natVal $ Proxy @(PNatToNat (NParams k))
, constructors = getValueConstructors @_ @sop
}
type HasElmStructure k x = HasElmStructure' k x (RepK x)
{- | Extract the structure of the representation of a datatype in Elm.
Used by code generation.
-}
getElmStructure :: forall {k} (x :: k). (HasElmStructure k x) => DatatypeStructure x
getElmStructure = getElmStructure' @k @x @(RepK x)
-- Extraction logic.
--
-- We essentially pattern-match on the Generic representation to extract:
--
-- - Elm metadata attached via the @ElmDeclarable@ class (type name and module name).
-- - Constructors (GElmSum).
-- - Fields, their names, and their types (potentially involving type variables).
class GElmSum (sop :: k) where
getValueConstructors :: [Constructor]
instance
( KnownSymbol valConName
, GElmProduct fields
) =>
GElmSum (M1 GHC.C ('GHC.MetaCons valConName 'GHC.PrefixI isNt) fields)
where
getValueConstructors =
[ Constructor
{ constructorName = symbolToText @valConName
, constructorFields = getFields @_ @fields
}
]
instance
( KnownSymbol valConName
, GElmProduct fields
, GElmSum otherCons
) =>
GElmSum (M1 GHC.C ('GHC.MetaCons valConName 'GHC.PrefixI isNt) fields :+: otherCons)
where
getValueConstructors =
( Constructor
{ constructorName = Text.pack $ symbolVal (Proxy @valConName)
, constructorFields = getFields @_ @fields
}
)
: getValueConstructors @_ @otherCons
instance
( GElmSum (s1 :+: s2)
, GElmSum otherCons
) =>
GElmSum ((s1 :+: s2) :+: otherCons)
where
getValueConstructors =
getValueConstructors @_ @(s1 :+: s2)
++ getValueConstructors @_ @otherCons
class GElmProduct (fields :: k) where
getFields :: [ElmField]
instance GElmProduct U1 where
getFields = []
instance
(GElmField (M1 GHC.S ('GHC.MetaSel mbFName u s l) fieldType)) =>
GElmProduct (M1 GHC.S ('GHC.MetaSel mbFName u s l) fieldType)
where
getFields = [getField @(M1 GHC.S ('GHC.MetaSel mbFName u s l) fieldType)]
instance (GElmProduct (f1 :*: f2), GElmProduct fields) => GElmProduct ((f1 :*: f2) :*: fields) where
getFields = (getFields @_ @(f1 :*: f2)) ++ (getFields @_ @fields)
instance (GElmField (M1 s m t), GElmProduct fields) => GElmProduct ((M1 s m t) :*: fields) where
getFields = (getField @(M1 s m t)) : (getFields @_ @fields)
class GElmField field where
getField :: ElmField
instance
(GElmFieldType Z fieldType, KnownSymbol fieldName) =>
GElmField (M1 GHC.S ('GHC.MetaSel (Just fieldName) u s l) (Field fieldType))
where
getField = (Just (symbolToText @fieldName), getTyRef @Z @fieldType PNil)
instance
(GElmFieldType Z fieldType) =>
GElmField (M1 GHC.S ('GHC.MetaSel Nothing u s l) (Field fieldType))
where
getField = (Nothing, getTyRef @Z @fieldType PNil)
class GElmFieldType (nParams :: PNat) fieldType where
getTyRef :: PList nParams TyRef -> TyRef
class HasNat (vn :: k) where
type ToNat vn :: Nat
-- GHC refuses a simple type family declaration (probably because the kinds vary).
instance HasNat VZ where
type ToNat VZ = 0
instance HasNat (VS vn) where
type ToNat (VS vn) = 1 + (ToNat vn)
instance (KnownNat (ToNat vn)) => GElmFieldType Z ('Var vn) where
getTyRef _ = TyRef (TyVar $ "a" <> Text.pack (show $ natVal $ Proxy @(ToNat vn))) []
instance (ElmDeclarable someType, nParams ~ NParams (KindOf someType)) => GElmFieldType nParams ('Kon someType) where
getTyRef params = mkTyRef @someType params
instance
(GElmFieldType Z t2, GElmFieldType (S n) t1) =>
GElmFieldType n (t1 :@: t2)
where
getTyRef params = getTyRef @(S n) @t1 ((getTyRef @Z @t2 PNil) `append` params)
append :: a -> PList n a -> PList (S n) a
append a PNil = a `PCons` PNil
append a (b `PCons` bs) = (b `PCons` (a `append` bs))