Skip to content

Instantly share code, notes, and snippets.

@amitu
Last active June 20, 2017 05:37
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save amitu/a71724c65b7a7a905c505199ce37b530 to your computer and use it in GitHub Desktop.
Save amitu/a71724c65b7a7a905c505199ce37b530 to your computer and use it in GitHub Desktop.
Elm Enhancement Proposal: Or (More of a thought experiment)
-- What if we have had an or keyword for creating types.
--
-- Motivation: I often have something that is of few other things.
--
-- Example 1 from my code:
-- this is my Main Msg. Main does not have any UI, it is an SPA, it delegates different
-- pages / Modules for UI and updated etc.
type Msg
= Login.Msg
| Masters.Msg
| Dashboard.Msg
| Support.Msg
-- Example 2: From same module
type Page =
LoginPage Login.Model
| MastersPage Masters.Model
| DashboardPage Dashboard.Model
| SupprtPage Support.Msg
type alias Model =
{ page : Page
}
-- If something can be Bool or String, or Bool or String or Int.
-- I try to create:
type StringOrIntOrBool =
StringValue String
| IntValue Int
| BoolValue Bool
type alias Column =
{ name : String
, value : StringOrIntOrBool
}
-- Proposal Part --
-- What if I could do:
type alias Column =
{ name : String
, value : String or Int or Bool
}
-- How would I use this?
displayValue : Column -> String
displayValue column =
case column.value of
String s ->
s
Bool b ->
toString b
Int i ->
toString i
-- What if we had types with type constructors?
type Shape a =
Square Int Int a
| Circle Int a
type alias Foo a =
{ name : String
, foo : Shape a or String
}
displayFoo : Foo a -> (a -> String) -> String
displayFoo foo fn =
case foo.foo of
String s ->
s
Shape (Circle _ a) ->
fn a
Shape (Square _ _ a) ->
fn a
-- Problem being solved: no need to create type constructors when not needed.
-- How would Json.Decode.Decoder look like?
foo : Json.Decode.Decoder Foo
foo =
Json.Decode.succeed foo
|: Json.Decode.field "name" Json.Decode.string
|: Json.Decode.field "foo" (shape `Json.Decode.or` Json.Decode.String)
or : Json.Decode.Decoder a -> Json.Decode.Decoder b -> Json.Decode.Decoder a or b
-- Json.Decode.or may be aliased to ||
import Json.Decode as JD exposing ((||), field, string)
foo : JD.Decoder Foo
foo =
JD.succeed foo
|: field "name" string
|: field "foo" (shape || string)
-- Here we *are* allowing or to be both a funciton name, and a keyword in case
-- of type decleration. Compiler will figure out when the or is being used, and
-- do the right thing.
-- FAQ --
-- How would `(Foo String) or (Foo Int)` work?
type alias Bar = Foo String or Foo Int
display : Bar -> String
display bar =
case bar of
Foo (String s) ->
s
Foo (Int i) ->
toString i
-- What if we had more complex scenarios?
BorF = BoolV Bool | FloatV Float
SorI = StringV String | BorFV BorF
type alias Bar = SorI or Int
display : Bar -> String
display bar =
SorI (StringV s) ->
s
SorI (BorFV (BoolV b)) ->
toString b
SorI (BorFV (FloatV f)) ->
toString f
Int i ->
toString i
-- How would you disambiguate between `Foo String or Foo Int` <-> `Foo (String or Int)`?
-- Answer: What if we do not, they are the same from the POV code that is accepting them.
-- Only question is how does compiler find out which is to be accepted.
-- We can have some theorems:
-- Theorem 1: `Foo String or Foo Int` == `Foo (String or Int)`
-- Theorem 2: `Foo String or Foo Int` is compatible with `Foo String or Foo Int or Foo Bool`.
-- Meaning if a function accepts later, former can be passed, but not vice versa.
-- This can be used for composition:
function1 : Int -> (String or Int)
function2 : (String or Int of Bool) -> String
function3 : Int -> String
function3 =
function1 >> function2
-- Can we mix and match | and or during type definition?
-- Not sure yet.
-- Can we get rid of | altogether in favour of or?
--
-- To answer that question, lets ask ourselves what does | really do?
--
-- | does two things:
-- 1. It creates a type. Or can also create a type.
-- 2. It creates a bunch of named constructors.
-- Lets look deeper. We think `type` is a union type. That is not
-- accurate:
type Username = Username String
-- Here we have not created a union type. Union requires plural, here there
-- is only one.
--
-- So type is not creating union types, its creating constructors. | is
-- letting us say there can be more than one constructs that create something.
--
-- The important point is, `type` as being used so far is not for the purpose
-- of creating unions, but a constructor factory.
--
-- Knowing all this, how would we reconcile | and or?
type alias UserInfo = {..}
type User
= Anonymous : -- : here is telling compiler to create a constructor with no args
or UserInfo
or Legacy : Int
-- Here we have expolited the symmetry of syntax, if Anonymous was defined as
-- a function, it would have had this signature.
Anonymous : User
-- We can make this more explicit:
type User
= Anonymous : User
or UserInfo
or Legacy : Int -> User
-- Can you give more examples?
-- Consider json-elm-schema/JsonSchema.Model.Schema definition:
type Schema
= Object ObjectSchema
| Array ArraySchema
| String StringSchema
| Integer IntegerSchema
| Number NumberSchema
| Boolean BooleanSchema
| Null NullSchema
| Ref RefSchema
| OneOf BaseCombinatorSchema
| AnyOf BaseCombinatorSchema
| AllOf BaseCombinatorSchema
| Lazy (() -> Schema)
| Fallback Json.Decode.Value
-- Here we have created meaningless constructors, Object and so on. Module name
-- spacing, does help us in not letting this become a problem in practice, but
-- we have still defined things (Object/Array/...) just because Elm forces us,
-- not because we needed them.
--
-- Sometimes we need type constructors, but often we create type constructors
-- just to satisfy elm.
type Schema
= ObjectSchema
or ArraySchema
or StringSchema
or IntegerSchema
or NumberSchema
or BooleanSchema
or NullSchema
or RefSchema
or OneOf : BaseCombinatorSchema
-- above line is equvalent to (from compiler pov)
or OneOf : BaseCombinatorSchema -> Schema
or AnyOf : BaseCombinatorSchema
or AllOf : BaseCombinatorSchema
or () -> Schema -- may be we still call it `Lazy : (() -> Schema) -> Schema` for clarity
or Json.Decode.Value
-- What else? Lets look at Maybe and Result modules/types.
display : Maybe User -> String
display : User or Maybe.Null -> String
-- are equivalent. This reduces the need of Maybe module.
-- Similarly
handle : Result Http.Error User -> String
-- vs
handle : Http.Error or User -> String
-- if error and success types happened to be indeed same, then we can resort to
handle : Result.Ok String or Result.Err String -> String
-- Sometimes constructors are useful, but quite often creating constructor is optional.
-- No need for Result type/module either.
-- RemoteData:
handle : RemoteData Http.Error User -> String
-- vs
handle : User or Http.Error or RemoteData.Loading or RemoteData.NotAsked -> String
-- Only purpose RemoteData is serving is to expose singletons .Loading and .NotAsked.
-- Maybe/Result/RemoteData define a bunch of map methods, we can have an or module
-- Or.elm
module Or exposing (map21, map22, map31, map32, map33, map41...)
map21 : (a -> c) : a or b -> c or b
map22 : (b -> c) : a or b -> a or c
-- and so on.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment