Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Divide (closed) Primitives and (opened) Builtins #119

Draft
wants to merge 8 commits into
base: master
Choose a base branch
from

Conversation

turboMaCk
Copy link
Member

@turboMaCk turboMaCk commented Sep 24, 2021

Splitting ElmPrim into:

  • ElmPrim - language level constructs
  • ElmBuiltins - types defined by libraries (not generated by elm-street) like elm/core or elm/time etc.

ElmPrim keeps its original closed design using variants in sum type.

ElmBuiltins is using record/product to make implementation open. This gives elm-street several new features:

  • user definable Elm instances for types provided by library
  • ability to define Elm instance for parametrized type at least in manual fashion.

Full vocabulary of ElmDefinition:

  • DefPrim - primitive types defined by elm (language itself - not just by core or other lib)
  • DefBuiltin - ADTs not generated by elm-street (provided by libraries like core or time).
  • DefRecord - record types
  • DefType - ADTs generated by elm-street

Example definitions

-- simple type instance
instance Elm Value where
    toElmDefinition _ = DefBuiltin $ ElmBuiltin
        { builtinImplType = "Value"
        , builtinImplEncoder = "Basics.identity"
        , builtinImplDecoder = "D.value"
        , builtinImplParams = []
        }

-- parametrized type instance
instance (Elm a, Elm b) => Elm (Either a b) where
    toElmDefinition _ = DefBuiltin $ ElmBuiltin
        { builtinImplType = "Result"
        , builtinImplEncoder = "elmStreetEncodeEither"
        , builtinImplDecoder = "elmStreetDecodeEither"
        , builtinImplParams = [elmRef @a, elmRef @b]
        }

Future work to be done

  • printing functions do produce valid elm code by are naive (produce trailing white-spaces)
  • Elm.Generate module should be extended to make it simpler to include custom imports into generated files. (future PR)

Possibly further extensions:

  • support for phantom types (labeling params as regular or phatom params)
  • in case we would be able to derive this, this can completely replace ElmType giving it support for parametrized types.

Related issues:

related closed issues:


@jhrcek This is not ready for merge but I figured it would be better to get your feedback as soon as I have working spike. Please feel free to comment. This PR resolves my single biggest frustration of working with elm-street.

@arbus I think you might also give input to this proposal

@turboMaCk turboMaCk marked this pull request as draft September 24, 2021 18:49
@turboMaCk turboMaCk changed the title Divide (closed) Primitives and (opend) Builtins Divide (closed) Primitives and (opened) Builtins Sep 24, 2021
Comment on lines -19 to +20
|> required "time" Iso.decoder
|> required "value" D.value
|> required "time" Iso.decoder
|> required "value" D.value
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

related to todo of making implementation less naive

Comment on lines -19 to +20
, ("time", Iso.encode x.time)
, ("value", Basics.identity x.value)
, ("time", Iso.encode x.time)
, ("value", Basics.identity x.value)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

related to todo of making implementation less naive

Comment on lines +6 to +7
type alias ElmStreetNonEmptyList a = (a, List a)

Copy link
Member Author

@turboMaCk turboMaCk Sep 24, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is now required as we can't produce type annotation (a, List a) - but we can produce annotations using this alias.

src/Elm/Ast.hs Outdated
Comment on lines 82 to 101
| ElmTime -- ^ @Posix@ in elm, @UTCTime@ in Haskell
| ElmValue -- ^ @Json.Encode.Value@ in elm, @Data.Aeson.Value@ in Haskell
| ElmMaybe !TypeRef -- ^ @Maybe T@
| ElmResult !TypeRef !TypeRef -- ^ @Result A B@ in elm
| ElmPair !TypeRef !TypeRef -- ^ @(A, B)@ in elm
| ElmTriple !TypeRef !TypeRef !TypeRef -- ^ @(A, B, C)@ in elm
| ElmList !TypeRef -- ^ @List A@ in elm
| ElmNonEmptyPair !TypeRef -- ^ @NonEmpty A@ represented by @(A, List A)@ in elm
deriving (Show)

-- | Buitin types defined by core or 3rd party libraries
-- Included definitions:
-- * @Maybe a@
-- * @Result a b@
-- * @List a@
-- * @Time.Posix@
-- * @Json.Encode.Value@
data ElmBuiltin = ElmBuiltin
{ builtinImplType :: !Text
, builtinImplEncoder :: !Text
, builtinImplDecoder :: !Text
, builtinImplParams :: ![TypeRef]
} deriving (Show)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is core of the PR

Comment on lines +137 to +183
instance Elm Value where
toElmDefinition _ = DefBuiltin $ ElmBuiltin
{ builtinImplType = "Value"
, builtinImplEncoder = "Basics.identity"
, builtinImplDecoder = "D.value"
, builtinImplParams = []
}

instance Elm UTCTime where
toElmDefinition _ = DefBuiltin $ ElmBuiltin
{ builtinImplType = "Posix"
, builtinImplEncoder = "Iso.encode"
, builtinImplDecoder = "Iso.decoder"
, builtinImplParams = []
}

instance Elm a => Elm (Maybe a) where
toElmDefinition _ = DefPrim $ ElmMaybe $ elmRef @a
toElmDefinition _ = DefBuiltin $ ElmBuiltin
{ builtinImplType = "Maybe"
, builtinImplEncoder = "elmStreetEncodeMaybe"
, builtinImplDecoder = "nullable"
, builtinImplParams = [elmRef @a]
}

instance (Elm a, Elm b) => Elm (Either a b) where
toElmDefinition _ = DefPrim $ ElmResult (elmRef @a) (elmRef @b)

instance (Elm a, Elm b) => Elm (a, b) where
toElmDefinition _ = DefPrim $ ElmPair (elmRef @a) (elmRef @b)

instance (Elm a, Elm b, Elm c) => Elm (a, b, c) where
toElmDefinition _ = DefPrim $ ElmTriple (elmRef @a) (elmRef @b) (elmRef @c)
toElmDefinition _ = DefBuiltin $ ElmBuiltin
{ builtinImplType = "Result"
, builtinImplEncoder = "elmStreetEncodeEither"
, builtinImplDecoder = "elmStreetDecodeEither"
, builtinImplParams = [elmRef @a, elmRef @b]
}

instance Elm a => Elm [a] where
toElmDefinition _ = DefPrim $ ElmList (elmRef @a)
toElmDefinition _ = DefBuiltin $ ElmBuiltin
{ builtinImplType = "List"
, builtinImplEncoder = "E.list"
, builtinImplDecoder = "D.list"
, builtinImplParams = [elmRef @a]
}

instance Elm a => Elm (NonEmpty a) where
toElmDefinition _ = DefPrim $ ElmNonEmptyPair (elmRef @a)
toElmDefinition _ = DefBuiltin $ ElmBuiltin
{ builtinImplType = "ElmStreetNonEmptyList"
, builtinImplEncoder = "elmStreetEncodeNonEmpty"
, builtinImplDecoder = "elmStreetDecodeNonEmpty"
, builtinImplParams = [elmRef @a]
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

utilize new ElmBuiltin in instances we provide out of the box.

Comment on lines +16 to +17
, time : Posix
, value : Value
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

related to todo of making implementation less naive

Copy link
Contributor

@jhrcek jhrcek left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't point to anything that's obviously wrong.
In general it feels that adding more extensibility in this way could open a can of worms.
But this might be fine if we're the only users of the library (just checked that on hackage elm-street is pretty low in terms of number of downloads https://hackage.haskell.org/packages/search?terms=elm)

I expected that making the library extensible would be pretty hard, if for no other reason then just because you're trying to provide API for creating definitions in some other general-purpose programming language.

The piece that is most unclear to me is "how to add more custom definitions/pre-existing decoders etc. to the generated ElmStree.elm to support more user-defined types"

src/Elm/Ast.hs Outdated Show resolved Hide resolved
-- * @Time.Posix@
-- * @Json.Encode.Value@
data ElmBuiltin = ElmBuiltin
{ builtinImplType :: !Text
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we change the field prefixes to elmBuiltin? The word Impl doesn't look very informative..

Copy link
Contributor

@jhrcek jhrcek Sep 29, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also I find the word "Builtin" a bit misleading.
It's feels to me to have the same meaning as "Elm primitive" as in "it's provided by Elm itself".
How about something like ElmLibrary / ElmLibType ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Impl is leftover from development. I did the change incrementally by porting support from simpler to more complicated types to new structure it will need to be removed.

It's feels to me to have the same meaning as "Elm primitive" as in "it's provided by Elm itself".

agree though even this is already improvement over including these things into primitives. The one way in which these things are built in even if they come from 3rd party library (say time which depends on elm-iso8601-date-strings package) is that their decoders and definition is not generated by elm-street. Including Lib into the name is also option. Though this can be even defined in modules other than the ones generated by elm street as well. This might need some more thinking perhaps.

-- * @Json.Encode.Value@
data ElmBuiltin = ElmBuiltin
{ builtinImplType :: !Text
, builtinImplEncoder :: !Text
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As a user of this API It's not clear from the type what this Text is supposed to be.
Is it name of the function defined somewhere? (Where?)
Can I add my custom definitions in that place?
Is it supposed to be fully qualified?
Although it's clear from the usage examples, it might not be so clear to library users ..

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should be documented, hover before user will be able to define own things free-rely we need to make it possible to define custom imports for generated modules. So it doesn't make much sense documenting this before that part redesign so whole process could be documented.

toElmDefinition _ = DefPrim $ ElmList (elmRef @a)
toElmDefinition _ = DefBuiltin $ ElmBuiltin
{ builtinImplType = "List"
, builtinImplEncoder = "E.list"
Copy link
Contributor

@jhrcek jhrcek Sep 29, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The E. and D. in this definition seem magical. Where are they coming from?
EDIT: Ah, I see they're coming from

import Json.Decode as D exposing (..)
import Json.Decode.Pipeline as D exposing (required)
...
import Json.Encode as E exposing (..)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes. I agreee this is not nice though. But I would probably rather address that in separate PR

Co-authored-by: Jan Hrcek <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants