agrafix / elm-bridge

Haskell: Derive Elm types from Haskell types
BSD 3-Clause "New" or "Revised" License
101 stars 27 forks source link

How to deal with UTCTime? #32

Open saurabhnanda opened 6 years ago

saurabhnanda commented 6 years ago

What's the best way to deal with UTCTime? I tried deriving the Elm definition and here's what showed-up on the Elm side:

type alias UTCTime  =
   { utctDay: Day
   , utctDayTime: DiffTime
   }

jsonDecUTCTime : Json.Decode.Decoder ( UTCTime )
jsonDecUTCTime =
   ("utctDay" := jsonDecDay) >>= \putctDay ->
   ("utctDayTime" := jsonDecDiffTime) >>= \putctDayTime ->
   Json.Decode.succeed {utctDay = putctDay, utctDayTime = putctDayTime}

jsonEncUTCTime : UTCTime -> Value
jsonEncUTCTime  val =
   Json.Encode.object
   [ ("utctDay", jsonEncDay val.utctDay)
   , ("utctDayTime", jsonEncDiffTime val.utctDayTime)
   ]

Is this the idiomatic way of dealing with timestamps on the Elm side? (I'm new to Elm, so please pardon this newbie question). If not, then how does one specify some sort of "mapping" between an idiomatic Haskell type and its corresponding idiomatic Elm counterpart?

saurabhnanda commented 6 years ago

Actually, just noticed a related issue. The type definitions and json-codecs of Day and DiffTime have not been generated. Is this expected?

bartavelle commented 6 years ago

There is the Date datatype. If the types do not have a ToJson instance, this would be real problematic to handle properly, and if they have, the best course of action would be to hardcode them in the Elm code generation and to add helper functions in json-helpers.

saurabhnanda commented 6 years ago

@bartavelle any existing examples that do what you are suggesting?

Also, is there any way to extend the code-gen (without patching this library) to use specific types on the Elm side (along with hand-written json codecs) for specific Haskell types?

bartavelle commented 6 years ago

Specific examples are all the "built-in" datatypes. You might want to check the default alterations. You then use the *WithAlterations functions and stack your own modifications by composing your own function with this one. Am I making sense, or this is all a bit fuzzy?

saurabhnanda commented 6 years ago

Going through the documentation index I could only find one *WithAlterations function, i.e. makeModuleContentWithAlterations -- is this the one that you are referring to?

So, the idea is to call makeElmModuleWithAlterations instead of makeElmModule and pass it a lambda that does the ETypeDef -> ETypeDef mapping. Within the lambda, I'm free to use defaultAlterations for the types that I don't want to alter. Did I get this correctly?

bartavelle commented 6 years ago

Yup! You can also do something like this, if you do not want to explicitly reference defaultAlterations in your own function:

makeElmModuleWithAlterations (myalterations . defaultAlterations)
saurabhnanda commented 6 years ago

@bartavelle okay, I got something working, but I'm scratching my head over the difference in behaviour of "top-level" and "nested" types. Here's an example based on Integer (that is baked into the library, at the moment):

Integer as top-level type

$(Elm.deriveElmDef defaultOptions ''Integer)
putStrLn $ makeElmModule "Foo" [DefineElm (Proxy :: Proxy Integer)]

--
-- output
--

module Foo exposing(..)

import Json.Decode
import Json.Encode exposing (Value)
-- The following module comes from bartavelle/json-helpers
import Json.Helpers exposing (..)
import Dict
import Set

type Integer  =
    S# Int#
    | Jp# BigNat
    | Jn# BigNat

jsonDecInteger : Json.Decode.Decoder ( Integer )
jsonDecInteger =
    let jsonDecDictInteger = Dict.fromList
            [ ("S#", Json.Decode.lazy (\_ -> Json.Decode.map S# (jsonDecInt#)))
            , ("Jp#", Json.Decode.lazy (\_ -> Json.Decode.map Jp# (jsonDecBigNat)))
            , ("Jn#", Json.Decode.lazy (\_ -> Json.Decode.map Jn# (jsonDecBigNat)))
            ]
        jsonDecObjectSetInteger = Set.fromList []
    in  decodeSumTaggedObject "Integer" "tag" "contents" jsonDecDictInteger jsonDecObjectSetInteger

jsonEncInteger : Integer -> Value
jsonEncInteger  val =
    let keyval v = case v of
                    S# v1 -> ("S#", encodeValue (jsonEncInt# v1))
                    Jp# v1 -> ("Jp#", encodeValue (jsonEncBigNat v1))
                    Jn# v1 -> ("Jn#", encodeValue (jsonEncBigNat v1))
    in encodeSumTaggedObject "tag" "contents" keyval val

Integer as a nested type

data MyFoo = MyFoo
  {
    foo1 :: Text
  , foo2 :: Integer
  } deriving (Eq, Show, Read)
$(Elm.deriveElmDef defaultOptions ''MyFoo)

putStrLn $ makeElmModule "Foo" [DefineElm (Proxy :: Proxy MyFoo)]

--
-- output
--

module Foo exposing(..)

import Json.Decode
import Json.Encode exposing (Value)
-- The following module comes from bartavelle/json-helpers
import Json.Helpers exposing (..)
import Dict
import Set

type alias MyFoo  =
   { foo1: String
   , foo2: Int
   }

jsonDecMyFoo : Json.Decode.Decoder ( MyFoo )
jsonDecMyFoo =
   ("foo1" := Json.Decode.string) >>= \pfoo1 ->
   ("foo2" := Json.Decode.int) >>= \pfoo2 ->
   Json.Decode.succeed {foo1 = pfoo1, foo2 = pfoo2}

jsonEncMyFoo : MyFoo -> Value
jsonEncMyFoo  val =
   Json.Encode.object
   [ ("foo1", Json.Encode.string val.foo1)
   , ("foo2", Json.Encode.int val.foo2)
   ]

Basically at the top-level, the defaultAlterations function is applied, and at nested/recursive levels, the recAlterType function is applied. And both have different behaviours. Any reason for why they have been designed this way?

saurabhnanda commented 6 years ago

Actually, just noticed a related issue. The type definitions and json-codecs of Day and DiffTime have not been generated. Is this expected?

After reading through the code of recAlterType I now see why this is happening, but I do not understand why the function doesn't "truly" descend a type recursively.

bartavelle commented 6 years ago

When you derive an elm encoding and generate the corresponding code for a type, it will create code that work on that particular type. You might want to manually write the code for "contained" types, or want to stop at "primitive" types (such as Integer).

If it worked recursively, you could not do that, and you would end up with unwanted code.

saurabhnanda commented 6 years ago

You might want to manually write the code for "contained" types, or want to stop at "primitive" types (such as Integer). If it worked recursively, you could not do that, and you would end up with unwanted code.

Apologies beforehand for pressing this further.

bartavelle commented 6 years ago

Hum, I am not sure I understood your question, so I will elaborate a bit and you'll tell me what I got wrong.

If you have this on the Haskell side:

data Foo = Foo Bar Baz

When you derive Foo, the generated elm code will be the Foo type definition, and the encoding/decoding functions for it. It will not derive code for Bar or Baz recursively, allowing you to replace them with your own definitions. If you want them generated, you will have to explicitely derive them and include them in the module.

Did I understand correctly that you are wondering why you have to derive Bar and Baz explicitely?

saurabhnanda commented 6 years ago

Did I understand correctly that you are wondering why you have to derive Bar and Baz explicitely?

Yes, this is precisely what I'm wondering.

bartavelle commented 6 years ago

Alright, so the answer is that if it did, that would be very annoying.

The reason is that many types have non-aeson Json instances, or that for some reason the user would like to handle it differently than the Haskell type.

For example, I work with a mixed Elm/JS code base, on a single Haskell backend. Some types must have a shape that I can't reproduce with Aeson, because of backward compatibility and how it is hard to refactor the JS code.

saurabhnanda commented 6 years ago

Consider another use-case, which is what I am struggling with currently:

Haskell data-structure:

data User
  = User {userCreatedAt :: !UTCTime,
          userUpdatedAt :: !UTCTime,
          userEmail :: !Types.Email,
          userRefreshToken :: !Data.Text.Internal.Text,
          userAccessToken :: !Data.Text.Internal.Text,
          userTokenExpiresAt :: !UTCTime}

Elm auto-gen hook (to convert UTCTime to Elm's Date):

makeModuleContentWithAlterations (myAlterations . defaultAlterations) 
  [ DefineElm (Proxy :: Proxy User) ]
  where
    myAlterations = case t of
      ETypeAlias (x@EAlias{ea_name=(ETypeName "UTCTime" [])}) -> ETypeAlias (x{ea_name=(ETypeName "Date" [])})
      _ -> t

Generated Elm type - notice, that the alterations have had no effect

type alias User  =
   { userCreatedAt: UTCTime
   , userUpdatedAt: UTCTime
   , userEmail: Email
   , userRefreshToken: String
   , userAccessToken: String
   , userTokenExpiresAt: UTCTime
   }

This is happening because myAlterations is called only for the top-level type, User, passed to makeModuleContentWithAlterations, not for each field/type in User.

This is what I was trying to refer to, in my comment - https://github.com/agrafix/elm-bridge/issues/32#issuecomment-398869632

saurabhnanda commented 6 years ago

Btw, here's the complicated workaround that I'm being forced to use, currently:

    myAlterations t = case t of
      ETypeAlias (x@EAlias{ea_fields=fields_}) ->
        let modifiedFields = (flip DL.map) fields_ $ \(fname, field_) -> case field_ of
              ETyCon (y@ETCon{tc_name="UTCTime"}) -> (fname, ETyCon $ y{tc_name="Date"})
              y -> (fname, y)
        in ETypeAlias (x{ea_fields=modifiedFields})
      _ -> t
bartavelle commented 6 years ago

Oh right, this is quite annoying indeed!

bartavelle commented 6 years ago

It is my fault, the documentation is lacking. You can use recAlterType to have a simpler representation and have it applied recursively.

bartavelle commented 6 years ago

That would be something like

myAlterations = recAlterType myTypeAlterations
myTypeAlterations t = case t of
  ETyCon (ETCon "UTCTime") -> ETyCon (ETCon "Date")
  _ -> t
bartavelle commented 6 years ago

There is also an example in the haddocks.

ibizaman commented 4 years ago

If it can be of any help, I ended up using a custom type instead https://gist.github.com/ibizaman/efa6ee8a7b4e0f5696c24ff493e5744a