better 1-constructor types
Similar to prior art:
A value is wrapped in the type Typed with a phantom tag.
→ A Typed ... Meters ... Float can't be called a Typed ... Kilos ... Float anymore!
For types with just one constructor with a value, a Typed can be a good replacement (↑ limits).
→ save boilerplate
extract =
\(Meters value) ->
value
map alter =
\(Meters value) ->
value |> alter |> Meters
...many more
naturalNumber |> NumberNatural.toInt
height =
3.2 |> Meters.fromFloat
height |> Meters.toFloat
(oneHeight |> Meters.toFloat)
+ (otherHeight |> Meters.toFloat)
|> Meters.fromFloatwith Typed
naturalNumber |> untag
height =
3.2 |> tag Meters
height |> untag
oneHeight
|> Typed.and otherHeight
|> Typed.map (\h0 h1 -> h0 + h1)TypedChecked → only "validated" values
module NumberNatural exposing (NumberNatural)
type NumberNatural
-- nobody outside this module can call this constructor
= NumberNatural Intcreating & altering NumberNaturals will only be possible inside that module
Tagged → attach a label
type Cat =
-- constructor can be used anywhere
Cat { name : String, mood : Mood }Users can create & alter new Cats everywhere
Public → allow users to access (→ untag) the value
Internal → hide the value from users
Tagged Publicimport Typed exposing (Typed, Tagged, Public, tag)
type alias Pet tag specificProperties =
Typed
Tagged
tag
Public
{ specificProperties | name : String, mood : Mood }
type alias Cat =
Pet CatTag { napsPerDay : Float }
type CatTag
= Cat
type alias Dog =
Pet DogTag { barksPerDay : Float }
type DogTag
= Dog
sit : Dog -> Dog
sit =
Typed.map (\d -> { d | mood = Neutral })howdy =
{ name = "Howdy", mood = Happy, napsPerDay = 2.2 }
|> tag Cat
howdy |> sit -- errorAnother example:
module Pixels exposing (Pixels, PixelsTag(..), ratio)
import Typed exposing (Typed, Tagged, Public, tag)
type alias Pixels =
Typed Tagged PixelsTag Public Int
type PixelsTag
= Pixels
ratio w h =
( w |> tag Pixels, h |> tag Pixels )module Window exposing (Window)
import Typed exposing (Typed, Tagged, Public, tag)
innerWidth =
700 |> tag Pixels
borderWidth =
5 |> tag Pixels
defaultWidth =
innerWidth
|> Typed.and borderWidth
|> Typed.map
(\( inner, border ) -> inner + border * 2)
defaultWidth |> untag
--> 710Checked Publicmodule Even exposing (Even, add, multiply, n0, n2)
import Typed exposing (Checked, Public, Typed, isChecked, tag)
type alias Even =
Typed Checked EvenTag Public Int
type
EvenTag
-- don't expose this constructor
= Even
multiply : Int -> Even -> Even
multiply factor =
\even ->
even
|> Typed.map (\int -> int * factor)
|> isChecked Even
add : Even -> Even -> Even
add toAddEven =
\even ->
(even |> Typed.and toAddEven)
|> Typed.map
(\( int, toAddInt ) -> int + toAddInt)
|> isChecked Even
n0 : Even
n0 =
0 |> tag Even
n2 : Even
n2 =
2 |> tag EvenThen outside this module
cakeForEven : Even -> Cake
Even.n0 |> Typed.map (\n -> n + 1) |> cakeForEven
--> compile-time error: is Tagged but expected Checked
Even.n2 |> Even.multiply -5 |> cakeForEven
--> CakeChecked InternalA validated value that can't be directly accessed by a user.
A module that only exposes randomly generated unique Ids:
module Id exposing (Id, random, toBytes, toString)
import Typed exposing (Typed, Checked, Internal)
import Random
type alias Id =
Typed Checked IdTag Internal (List Int)
type IdTag = Id
random : Random.Generator Id
random =
Random.list 2
(Random.int Random.minInt Random.maxInt)
|> Random.map (tag Id)
-- the API stays the same even if the implementation changes
toBytes --...
toString --...→ Outside of this module, the only way to create an Id is Id.random
Tagged Internalmodule Password exposing (PasswordUnchecked, PasswordGood, check, length, unchecked)
import Typed exposing (Typed, Tagged, Checked, Internal, tag, internal)
type alias Password goodOrUnchecked =
Typed goodOrUnchecked PasswordTag Internal String
type PasswordTag
= -- don't expose the tag constructor
Password
type alias PasswordGood =
Password Checked
type alias PasswordUnchecked =
Password Tagged
-- ! annotates the result as `Tagged` ↓
unchecked : String -> PasswordUnchecked
unchecked =
tag Password
check : PasswordUnchecked -> Result String PasswordGood
check =
\passwordToTest ->
let
passwordString =
passwordToTest |> internal Password
in
if (passwordString |> String.length) < 10 then
Err "Use at lest 10 letters & symbols."
else if commonPasswords |> Set.member passwordString then
Err "Choose a less common password."
else
passwordToTest |> isChecked Password |> Ok
commonPasswords =
Set.fromList
[ "password1234", "secret1234"
, "c001_p4ssw0rd", "1234567890"
--...
]You can then decide that only a part of the information should be accessible.
-- doesn't expose too much information.
length : Password goodOrUnchecked_ -> Int
length =
\password ->
password
|> internal Password
|> String.lengthmodule Register exposing (Model, Event, ui, reactTo, modelInitial)
import Password exposing (PasswordUnchecked)
type alias Model =
{ -- accessing user-typed password is impossible
passwordTyped : PasswordUnchecked
, loggedIn : LoggedIn
}
modelInitial : Model
modelInitial =
{ passwordTyped =
"" |> Password.unchecked
, loggedIn = NotLoggedIn
}
type LoggedIn
= -- no user can have an unchecked password
LoggedIn { userPassword : PasswordGood }
| NotLoggedIn
type Event
= PasswordEdited PasswordUnchecked
| PasswordConfirmed PasswordGood
reactTo : Event -> Model -> Model
reactTo event =
\model ->
case event of
PasswordEdited uncheckedPassword ->
{ model
| passwordTyped = uncheckedPassword
}
PasswordConfirmed passwordGood ->
{ model
| passwordTyped =
"" |> Password.unchecked
, loggedIn =
LoggedIn { userPassword = passwordGood }
}
ui =
\{ passwordTyped } ->
[ [ "register" |> Html.text ] |> Html.div []
, Html.input
[ onInput
(\text ->
text
|> Password.unchecked
-- not accessible from now on
|> PasswordEdited
)
, String.repeat
(passwordTyped
|> Password.length
)
"·"
|> Html.value
]
[]
, case passwordTyped |> Password.check of
Ok passwordGood ->
Html.button
[ onClick (PasswordConfirmed passwordGood) ]
[ "Create account" |> Html.text ]
Err message ->
message |> Html.text
]
|> Html.div []
passwordTyped |> untag |> leak
userPassword |> untag |> leak→ compile-time error: expected Public but found Internal
CheckedMore often than not, there's already a type with the same promises even when created directly by users:
type alias StringFilled =
- Typed Checked StringFilledTag Public String
+ Hand { head : Char, tail : String } Never Empty
type alias PasswordGoodInternal =
- String
+ Arr (Min Nat10) CharUsed here:
This package wouldn't exist without inspiration:
especially
Checked Internal value is a major changeFor many this might be a deal-breaker.
Be explicit and choose a type for parts of information that could be added or removed in the future.
type alias Comment =
Typed
Tagged
CommentTag
Public
{ message : String
, responses : List Comment
}elm:
This type alias is recursive, forming an infinite type.
Somewhere in that cycle, you need to define an actual type to end the infinite expansion.
In this instance: try tree structures like zwilias/elm-rosetree:
type alias Comments =
Maybe (Tree { message : String })From the outside, recursive aliases seem like a problem solvable at the language level. Let's watch how elm handles them in the future.