Last active
June 20, 2017 05:37
-
-
Save amitu/a71724c65b7a7a905c505199ce37b530 to your computer and use it in GitHub Desktop.
Elm Enhancement Proposal: Or (More of a thought experiment)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
-- 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