Elm union subsets - elm

Say I have a union type like this:
type Route
= Home
| License
| UserProfile { username : String }
| Search { query : String }
| SomeOtherPage
In practice I frequently need to work with subsets of this union. For example:
type StaticRoute = Home | License
I would like to be able to define functions which accept subsets like the above, instead of the wider Route.
I don't want to nest StaticRoute inside of Route, like so:
type Route
= Static StaticRoute
| UserProfile { username : String }
| Search { query : String }
| SomeOtherPage
This is because I want to be able to define many different subsets of Route, some of which could overlap:
type StaticRoute = Home | License
type RouteWithServerRendering = Home | Search { query : String }
type LoggedInRoute = SomeOtherPage
-- and so on…
How then can I define subsets of Route without repeating definitions?

Jasper Woudenberg recently posted Conversion functions, five stars
, which advocates for having similar types and using conversion functions to translate between one type to another.
In your case, it might look like this:
module Route exposing (fromStaticRoute, toStaticRoute)
fromStaticRoute : StaticRoute -> Route
fromStaticRoute staticRoute =
case staticRoute of
Static.Home ->
Home
Static.License ->
License
toStaticRoute : Route -> Maybe StaticRoute
toStaticRoute route =
case route of
Home ->
Just Static.Home
License ->
Just Static.License
_ ->
Nothing

Related

Function with extensible record parameter passed as a parameter cannot be used from within a case statement

I'm trying to extract some information from some records using extensible records. If I create a function taking the extensible record type and returning a string and use that within a case statement then there are no issues (namedToString, in the below example). However, if I attempt to use a function passed as a parameter (stringFromNamed) I will get an error complaining:
This business value is a:
Business
But stringFromNamed needs the 1st argument to be:
Named a -> String (edited)
from the example code below:
type alias Named a =
{ a | name : String }
type alias Person =
Named { address : String }
type alias Business =
Named { employeeCount : Int }
type Change
= PersonUpdate Person
| BusinessUpdate Business
namedToString : Named a -> String
namedToString changeFields =
changeFields.name
changeToString : (Named a -> String) -> Change -> String
changeToString stringFromNamed change =
case change of
PersonUpdate person ->
-- This works
namedToString person
BusinessUpdate business ->
-- This will cause the error
stringFromNamed business
This example includes just the required code but a more complete example can be found at https://ellie-app.com/77nCPLh55j3a1
What is causing the issue and how can I achieve my goal of passing in a function which will extract some information from an extensible record?
I discovered this issue in the Elm compiler repo: https://github.com/elm/compiler/issues/1959
This appears to be a bug in the compiler which can be worked around if you just want to pass an external record of one type to a function parameter. You can do this by removing the type signature of the higher order function, changeToString in the example above.
Unfortunately, if we wanted to perform the same action from the function parameter, e.g. stringFromNamed, on every case the error will return so another workaround must be found.
The workaround I'm using is to create a new record type which includes exactly the fields from the extensible record and then creating an instance of this from the fields of the other records adhering to the extensible record type. Not a big problem with only a couple of cases and an extensible record with only one field but this doesn't scale especially well. Example below:
type alias Named a =
{ a | name : String }
type alias OnlyNamed =
Named {}
type alias Person =
Named { address : String }
type alias Business =
Named { employeeCount : Int }
type Change
= PersonUpdate Person
| BusinessUpdate Business
type Msg
= UpdateCurrentChange Change
namedToString : Named a -> String
namedToString changeFields =
changeFields.name
changeToString : (OnlyNamed -> String) -> Change -> String
changeToString stringFromNamed change =
case change of
PersonUpdate { name } ->
stringFromNamed { name = name }
BusinessUpdate { name } ->
stringFromNamed { name = name }
I think the issue is that the function you're passing in takes Named a, and Elm doesn't know what the a refers to, so it ends up deciding it's a type mismatch. If you change the type annotation to (Named Business -> String) -> Change -> String it will compile and work.

Elm: accessing common fields in union

I am trying to model a type as a union where each member of that union has properties in common with all other members.
I am currently achieving this like so:
type alias File = {
name : String
}
type CommonFileState extra = CommonFileState {
id : String
, file : File
} extra
type alias ValidFileState = CommonFileState {
validatedAt : Int
}
type alias InvalidFileState = CommonFileState {
reason : String
}
type alias LoadingFileState = CommonFileState {}
type FileState = Valid ValidFileState | Invalid InvalidFileState | Loading LoadingFileState
Now if I want to read one of those common properties on any given FileState, I must match against each member of the union:
getId : FileState -> String
getId fileState = case fileState of
Valid (CommonFileState {id} extra) -> id
Invalid (CommonFileState {id} extra) -> id
Loading (CommonFileState {id} extra) -> id
This feels wrong to me, because I have to duplicate the property access for each member. If I needed to manipulate this property somehow (e.g. concatenating something onto the string), I would also have to duplicate this.
I want to be able to easily access common properties of my union, and operate on those common properties.
When I started searching for other ways to do this, I found one alternative was to nest the union inside a record, which also holds the common properties:
type alias ValidCurrentFileState = {
validatedAt : Int
}
type alias InvalidCurrentFileState = {
reason : String
}
type alias LoadingCurrentFileState = {}
type CurrentFileState = Valid ValidCurrentFileState | Invalid InvalidCurrentFileState| Loading LoadingCurrentFileState
type alias File = {
name : String
}
type alias FileState = {
id : String
, file : File
, currentState : CurrentFileState
}
getId : FileState -> String
getId {id} = id
However this is awkward because I have to name the nested union, which adds a level of unnecessary indirection: "file state" and "current file state" are conceptually the same.
Are there any other ways of doing this which don't have the problems I mentioned?
I think you are thinking about this the wrong way around.
The purpose of modelling (in Elm) is capture the possible states of your data, and to exclude - in your model - 'impossible' states, so that the compiler can statically prevent the code every creating such states.
Once you're happy with your model, you write the helpers you need to make your core logic easy to express and to maintain.
I suspect I would normally go with your second approach, but I don't know all the issues you need to account for.

TYPE MISMATCH - This function cannot handle the argument sent through the (|>) pipe:

I am a super elm begginer and trying to make app.
Currently I am struggling to make landing page and http request to a server.
But, I am stuck here...
I have init function something like this below.
init : () -> Url.Url -> Nav.Key -> ( Model, Cmd Msg )
init flags url key =
Model key TopPage
|> goTo (Route.parse url)
The definition of my Model is below.
-- MODEL
type alias Model =
{ key : Nav.Key
, page : Page
, name : String
, tags : List Tag
, jwt : String }
and, goTo function is below.
goTo : Maybe Route -> Model -> ( Model, Cmd Msg )
goTo maybeRoute model =
case maybeRoute of
Nothing ->
( { model | page = NotFound }, Cmd.none )
Just Route.Top ->
( { model | page = TopPage }, Cmd.none )
...
type Route is below.
type Route
= Top
| User String
| Repo String String
parse : Url -> Maybe Route
parse url =
Url.Parser.parse parser url
parser : Parser (Route -> a) a
parser =
oneOf
[ map Top top
, map User string
, map Repo (string </> string)
]
but following error has occured.
-- TYPE MISMATCH -------------------------------------------------- src/Main.elm
This function cannot handle the argument sent through the (|>) pipe:
54| Model key TopPage
55| |> goTo (Route.parse url)
^^^^^^^^^^^^^^^^^^^^^
The argument is:
String -> List Tag -> String -> Model
But (|>) is piping it a function that expects:
Model
What did I make mistake here?....
Your Model type has five fields, but in the line
Model key TopPage
you are only providing values for the first two of the five. You are missing values for the name, tags and jwt fields. Provide values for these and the problem should go away.
When you declare a type alias such as Model, Elm creates a constructor function also named Model. Elm functions support partial application, in that if you pass in values for some but not all of the arguments, you end up with a function that takes in the rest of the values. You provided two arguments, so you end up with a function that takes three arguments and returns a Model.
There are two ways of building a value of a type. Given a simple example of a Person type alias:
type alias Person = { name : String, age : Int }
You can construct a value by specifying all fields (note that you don't have to specify Person in the constructor; Elm's compiler is smart enough to know it by its shape):
jane : Person
jane = { name = "Jane", age = 35 }
Or you can build a value by using the type name and specify each field's values in the order in which they were defined. In this style, you can think of Person acting like a function with two parameters that returns a Person value.
jane : Person
jane = Person "Jane" 35
In each case, you have to specify all fields of the type when you construct it in order to obtain a complete Person value. However, that is not the complete story. It is possible to leave off the age parameter when constructing a Person, but the result isn't a Person, it's a function that takes an age and returns a Person. In other words,
janeAged : Int -> Person
janeAged = Person "Jane"
You can strip off as many parameters from the end as you'd like to make more variations on that constructor, even stripping out all parameters:
somebody : String -> Int -> Person
somebody = Person
Back to your example. You are constructing a Model value by only specifying two parameters (Model key TopPage). The value of that expression does not result in a Model, but in a function that takes three more parameters to create a Model. And that's why the error message indicated you need three parameters to construct a model.
You need to specify all values of Model when creating it.

Is it possible to use Maybe with Extensible Records?

I am trying to write a function to pull the maybe off a list of extensible records, I am wondering if this is possible. Source code is below, or see Ellie link here
module Temp exposing (..)
import Html exposing (text)
main =
text "Hello"
items : Maybe List { data | id : Int } -> List { data | id : Int }
items maybeList =
case maybeList of
Just t ->
t
Nothing ->
[]
Maybe List { data | id : Int } parses as Maybe (List) ({ data | id : Int }). I'm not sure why the error message is so misleading, but the fix is to wrap List ... in () like this:
items : Maybe (List { data | id : Int }) -> List { data | id : Int }
^ ^
Edit: also, your function can be simplified using Maybe.withDefault:
items = Maybe.withDefault []

How to accomplish inheritance in Elm

type alias Car = { model : Model, year: Int }
type Model = Crv | Enzo | Mustang | Viper
-- I can do this
enzo = Car Enzo -- partial Contructor
> myE<function> : Int -> Repl.Car
myEnzo = enzo 2018
> { model = Enzo, year = 2018 } : Repl.Car
-- but i CANNOT do this
enzo2017 = Car { year: 2017 } -- INVALID
How can i make a simple constructor that doesn't force me to list the attributes in a defined order.
When it comes to functional programming we need to "start over" with some concepts. In this case the confusion seems to be with the word constructor.
The record constructor in Elm is different than the object constructor in object oriented language.
The record constructor does not create a record, it only creates a function that can create records. It does this by partial application.
In partial application you can only give the arguments in their order. This is defined in lambda calculus and I believe it is not specific to Elm.
In the case of creating records with record constructor changing the order of the argument doesn't make changes in the result.
We can use this handy function flip that returns the same function with reverse order of the arguments:
enzo2017 = flip Car 2017
myEnzo2017 = enzo2017 Viper
Consider, when you define this:
type alias Car = { model : Model, year: Int, notes: String}
what you really get, is a function Car like this:
> Car
> <function> : Repl.Model -> Int -> String -> Repl.Car
It's easy to define another constructor:
> f notes model year = Car model year notes
> <function> : String -> Repl.Model -> Int -> Repl.Car