gampleman/elm-review-derive/1.0.0

CodeGenerator

This module let's you define type-oriented principled code generators.

By type oriented we mean generators that are driven by a type definition provided by the user.

By principled we mean that the generated code will for the foremost follow compositional patterns to be able to express (almost) any type.

type alias CodeGenerator =
CodeGenerator

Represents a code generator configuration.

{ id : String
, dependency : String
, typePattern : TypePattern
, makeName : String -> String
}
-> List (Definition ())
-> CodeGenerator

Create a code generator. This requires the following pieces:

  • a unique id. This id can be used to extend the generator later.
  • a dependency name which specifies what this generator deals with. This generator will only be active if the user has that dependency installed.
  • a TypePattern that is used to figure out which type the function should work on.
  • a function that generates names if the generator needs to make an auxiliary definition
  • a list of Definitions that determine how code is actually generated. Note that later definitions will override previous ones.

Defining code generators

type Definition a

Definitions are a way to to generate and compose small snippets of code to handle specific situations that might occur in an Elm type. Fundamentally you can think of all the definitions put together as forming a rather sophisticated function ResolvedType -> Expression, however this library will handle a large number of gotcha's for you, so it's more convenient to define the function piece-meal.

Apply this definition conditionally if the user has this specific dependency installed (can be chained). Intended for things like json-pipeline or random-extra.

Using existing functions

The code generator will find matching functions in the users code and dependencies. It will divided them into multiple levels and prioritise like this:

  1. Values defined in the current file
  2. Values defined in other project modules that the current file imports.
  3. Values defined in dependency modules that the current file imports.
  4. Values defined in dependencies.
  5. Values defined in other project modules.
  6. Values hard-coded in the code generator definition.

However, it will reject functions where multiple matching functions exists at the same level.

use : String -> Definition a

If there are multiple existing functions at the same level, this allows you to bless one of these to be used as the default implementation. Takes a fully qualified name.

Primitives

bool : Expression -> Definition a

Handle an Bool type.

int : Expression -> Definition a

Handle an Int type.

float : Expression -> Definition a

Handle a Float type.

string : Expression -> Definition a

Handle a String type.

char : Expression -> Definition a

Handle a Char type.

list : (Expression -> Expression) -> Definition a

Handle a List a type. You will be given code that handles the a subtype.

array : (Expression -> Expression) -> Definition a

Handle a List a type. You will be given code that handles the a subtype.

set : (Expression -> Expression) -> Definition a

Handle a List a type. You will be given code that handles the a subtype.

dict : (Expression -> Expression -> Expression) -> Definition a

Handle a Dict.

maybe : (Expression -> Expression) -> Definition a

Handle a Maybe a type. You will be given code that handles the a subtype.

( ( ResolvedType, Expression )
-> ( ResolvedType, Expression )
-> Expression
)

Handle a Dict, but get information about the types. This is useful, since sometimes we need the type of the keys.

Tuples

unit : Expression -> Definition a

Handle the unit () type.

tuple : (Expression -> Expression -> Expression) -> Definition a

Deals with 2-tuples (i.e. pairs). No need to implement if you have map2, as it will automatically be used.

triple : (Expression -> Expression -> Expression -> Expression) -> Definition a

Deals with 3-tuples (i.e. triples). No need to implement if you have map3, as it will automatically be used.

Combining values

succeed : (Expression -> Expression) -> Definition a

Wrap a value in the type. This is called different things in different libraries (i.e. List.singleton, Random.constant, etc.)

map : (Expression -> Expression -> Expression) -> Definition a

Transform a value inside a type. You will be handed the arguments.

Int
-> (String -> Expression -> List Expression -> Expression)

A convenient way to specify map2, map3, map4, etc.

The first argument specifies up to what number of arguments you want to specify the mapN.

The first argument in the callback is the standard name, so for 3 arguments you will get "map3".

(Expression -> Expression)
-> (Expression -> Expression)

Deal with any number of arguments using applicative style. The first argument is like for succeed, the second is a partially applied andMap.

(ResolvedType -> Expression -> List Expression -> Maybe Expression)

If map, mapN, succeed, pipeline don't work for you, this is a more custom way to combine these.

The arguments that the function you pass will recieve are:

  1. Information about the type being constructed (e.g. Foo Int).

  2. An expression representing a function that creates the type in question (e.g. makeFoo : Int -> Foo).

  3. A list of expressions that have already been generated (e.g. [ Decode.int ])

Dealing with custom types

( List ( Reference, List ResolvedType )
-> List ( String, Expression )
-> Expression
)

Deal with custom types. You will get a list of ( constructorName, expressionThatGeneratesTheTypeWithThatConstructor ).

The challenge is to work out which of the branches should be chosen. You can solve that with a andThen, or the library might have a different mechanism for disjunctions.

lambdaBreaker : (Expression -> Expression) -> Definition a

Elm only allows recursive definitions if there is a lambda somewhere in the chain. For instance:

naiveListDecoder : Decoder (List Int)
naiveListDecoder =
    Decode.oneOf
        [ Decode.map2 (::) (Decode.index 0 Decode.int) (Decode.index 1 naiveListDecoder)
        , Decode.null []
        ]

would fail to compile, as this would crash immediately on calling the program with an infinite loop. Incidentally this would compile:

naiveListDecoder2 : Decoder a -> Decoder (List a)
naiveListDecoder2 childDecoder =
    Decode.oneOf
        [ Decode.map2 (::) (Decode.index 0 childDecoder) (Decode.index 1 (naiveListDecoder2 childDecoder))
        , Decode.null []
        ]

But the code would fail at runtime with a Maximum Call Stack Exceeded exception. However, this version would work fine:

smartListDecoder : Decoder (List Int)
smartListDecoder =
    Decode.field "length" Decode.int
        |> Decode.andThen
            (\l ->
                case l of
                    0 ->
                        Decode.succeed []


                    2 ->
                        Decode.map2 (::) (Decode.index 0 Decode.int) (Decode.index 1 smartListDecoder)


                    _ ->
                        Decode.fail "Unexpected list length"
            )

The reason being the lambda, that is only evaluated when a preceding step succeeds, hence there will be no infinite evaluation.

Of course, most libraries have a solution to this problem, typically called lazy, which we could use here:

smartListDecoder2 : Decoder (List Int)
smartListDecoder2 =
    Decode.oneOf
        [ Decode.map2 (::) (Decode.index 0 Decode.int) (Decode.index 1 (Decode.lazy (\() -> smartListDecoder2))
        , Decode.null []
        ]

However, if there is no lazy function, it can be implemented in terms of andThen and succeed:

lazy fn =
    andThen fn (succeed ())

We call this lazy function a lambdaBreaker, since it's purpose it to break recursion with a lambda. Implementing it will enable the code generator to deal with recursive types.

Going crazy

custom : (ResolvedType -> Maybe Expression) -> Definition a

This allows you complete freedom in generating expressions, however it also doesn't give you much help.

The recommendation here is to use the normal definitions and only use this for exceptional cases.

Advanced use

{ id : String
, dependency : String
, typePattern : TypePattern
, makeName : Maybe (String -> String)
, input : a
}
-> List (Definition a)
-> CodeGenerator

Like define, but this allows you to compute as you progress down the tree of types. Also note that makeName now takes a Maybe. If you pass Nothing, than the generator will not create any auxiliary definitions, but will rather generate everything inline.

(a -> ResolvedType -> List ResolvedType -> List a)
-> ( a
-> Expression
-> List Expression
-> Maybe Expression
)

Like combiner, but allows you to control how input flows to the children and also receives input from its parent.

(a -> List ( Reference, List ResolvedType ) -> List a)
-> ( a
-> List ( Reference, List ResolvedType )
-> List ( String, Expression )
-> Expression
)

Like customType, but allows you to control how input flows to the children and also receives input from its parent.