For a type
with just 1 variant,
Typed
is a convenient, safe replacement (↑ limits)
Attach a tag
so a Typed .. Meters .. Float
isn't accepted as Typed .. Kilos .. Float
anymore.
type
boilerplate ↓ is covered by Typed
quantity =
\(Meters quantity) ->
quantity
alter quantityAlter =
\(Meters quantity) ->
quantity |> quantityAlter |> Meters
and other helpers like mapping multiple etc.
Plus you don't have to spell out the obvious:
3.2 |> Meters.fromFloat
prime |> Prime.toInt
height |> Meters.toFloat
(oneHeight |> Meters.toFloat)
+ (otherHeight |> Meters.toFloat)
|> Meters.fromFloat
with Typed
3.2 |> tag Meters
prime |> untag
height |> untag
oneHeight
|> Typed.and otherHeight
|> Typed.map (\( h0, h1 ) -> h0 + h1)
Kinds of Typed
-
Tagged
→ attach a label things like when you'd use-- module Cat exposing (Cat(..)) type Cat = -- variant can be used anywhere Cat { name : String, mood : Mood }
Users can create & alter new
Cat
s everywhereA
type(..)
can't expose the variant for creating & altering without allowing access as well.Typed
can, as we'll see in sectionTagged Internal
-
Checked
→ only "validated" things like when you'd use-- module Prime exposing (Prime) type Prime = -- nobody outside this module can use this variant Prime Int
creating & altering
Prime
s will only be possible inside thatmodule
An opaque
type
can't expose the variant for destructuring only.Typed
can, as we'll see in sectionChecked Public
import Typed exposing (Typed, Tagged, Public, tag)
type alias Cat =
Typed Tagged CatTag Public { name : String, mood : Mood, napsPerDay : Float }
type CatTag
= Cat
type alias Dog =
Typed Tagged DogTag Public { name : String, mood : Mood, barksPerDay : Float }
type DogTag
= Dog
sit : Dog -> Dog
sit =
Typed.map (\d -> { d | mood = Neutral })
howdy : Cat
howdy =
{ name = "Howdy", mood = Happy, napsPerDay = 2.2 }
|> tag Cat
howdy |> sit -- error
Another example:
-- module Pixels exposing (Pixels, PixelsTag(..))
import Typed exposing (Typed, Tagged, Public, tag)
type alias Pixels =
Typed Tagged PixelsTag Public Int
type PixelsTag
= Pixels
-- in another module using Pixels
innerWidth : Pixels
innerWidth =
700 |> tag Pixels
borderWidth : Pixels
borderWidth =
5 |> tag Pixels
defaultWidth : Pixels
defaultWidth =
innerWidth
|> Typed.and borderWidth
|> Typed.map
(\( inner, border ) -> inner + border * 2)
defaultWidth |> Typed.untag
--> 710
-- module Even exposing (Even, n0, n2, add, multiplyBy)
import Typed exposing (Typed, Checked, Public, tag)
type alias Even =
Typed Checked EvenTag Public Int
-- don't expose(..) its variant
type EvenTag
= Even
multiplyBy : Int -> Even -> Even
multiplyBy factor =
\even ->
even
|> Typed.map (\int -> int * factor)
|> Typed.toChecked Even
add : Even -> Even -> Even
add toAddEven =
\even ->
even
|> Typed.and toAddEven
|> Typed.map
(\( int, toAddInt ) -> int + toAddInt)
|> Typed.toChecked Even
n0 : Even
n0 =
0 |> tag Even
n2 : Even
n2 =
2 |> tag Even
-- in another module using Even
cakeForEven : Even -> { cake : () }
cakeForEven _ =
{ cake = () }
n0 |> Typed.map (\n -> n + 1) |> cakeForEven
--→ compile-time error: is Tagged but expected Checked
n2 |> multiplyBy -5 |> cakeForEven
--> { cake = () }
Above example is just for illustration! In practice, prefer a narrow type
type Even
= Times2 Int
A validated thing that can't be directly accessed by a user.
A module that only exposes randomly generated unique Id
s:
-- module Id exposing (Id, random, toBytes, toString)
import Typed exposing (Typed, Checked, Internal, tag)
import Random
type alias Id =
Typed Checked IdTag Internal (List Int)
type IdTag
= Id
random : Random.Generator Id
random =
Random.list 4
(Random.int 0 (2 ^ 32 - 1))
|> 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
Again, above example is just for illustration!
In practice, prefer a narrow type
as shown in elm-bits
type alias Id =
ArraySized (Exactly N128) Bit
-- module Password exposing (PasswordUnchecked, PasswordGood, toChecked, 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 variant
Password
type alias PasswordGood =
Password Checked
type alias PasswordUnchecked =
Password Tagged
-- ! annotates the result as `Tagged` ↓
unchecked : String -> PasswordUnchecked
unchecked =
tag Password
toChecked : PasswordUnchecked -> Result String PasswordGood
toChecked =
\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 |> Typed.toChecked 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.length
used in
-- module Register exposing (State, Event, ui, reactTo, stateInitial)
import Password exposing (PasswordUnchecked)
type alias State =
{ -- accessing user-typed password is impossible
passwordTyped : PasswordUnchecked
, loggedIn : LoggedIn
}
stateInitial : State
stateInitial =
{ 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 =
case event of
PasswordEdited uncheckedPassword ->
\model ->
{ model
| passwordTyped = uncheckedPassword
}
PasswordConfirmed passwordGood ->
\model ->
{ 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.toChecked 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
This package wouldn't exist without inspiration:
especially
the type of the Public
untagged thing is not obvious but used often
In that case expose more descriptive API and leave the rest as "safe internals"!
If you strictly want to avoid allowing untag
under all circumstances,
make it Internal
toDescriptiveValue : TypedThing -> DescriptiveValue
toDescriptiveValue =
Typed.internal ThingTag
always prefer narrow type over Checked
More often than not, there's already a type with the same promises even when created directly by users:
Instead of
type alias StringFilled =
Typed Checked StringFilledTag Public String
type alias PasswordLongEnough =
Typed Checked PasswordLongEnoughTag Public String
make it safe
type alias StringFilled =
{ head : Char, tail : String }
type alias PasswordLongEnough =
ArraySized (Min (Fixed N10)) Char
Here using typesafe-array
Use those! Extensively. No opaque type or Checked
necessary
All ↓ aren't breaking in practice but result in a major version bump
For many package authors, this is 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.