lue-bird / elm-typed-value / Typed

Labelled wrapper


type alias Typed creator tag accessRight untyped =
Internal.Typed creator tag accessRight untyped

A tagged thing.

→ A Typed ... Meters ... Float isn't accepted as Typed ... Kilos ... Float anymore!

For a type with just 1 variant, a Typed can be a safe replacement. It'll save some boilerplate.

A Typed knows

More detailed explanations and examples → readme

reading types

map :
    (untyped -> untypedMapped)
    -> Typed creator_ tag accessRight untyped
    -> Typed Tagged tag accessRight untypedMapped

Note: Just like with opaque types, calling == is always possible, even on Internal Typeds to

If you really need to prevent users from finding out the inner thing without using untag or internal, try

create

tag : tag -> untyped -> Typed creatorChecked_ tag accessRightPublic_ untyped

Create a new Typed from its raw thing and the tag. Rights can be chosen by annotating or using it as

passwordSafe =
    tag NowItsSafe "secret1234 th1s iS private oO"

passwordSafe |> Typed.untag
--→ "secret1234 th1s iS private oO" oh no!

instead

passwordSafe : Password -- important!
passwordSafe =
    tag NowItsSafe "secret1234 th1s iS private oO"

passwordSafe |> Typed.untag
-- compile-time error

type alias Password =
    Typed Checked PasswordTag Internal String

type PasswordTag
    = NowItsSafe

passwordSafe |> Typed.internal NowItsSafe
--> "secret1234 th1s iS private oO"
-- allowed because we could prove we're in the internal module

creator


type alias Tagged =
Internal.Tagged

Anyone is able to create a Typed Tagged ....

Example: Typed Tagged MetersTag ... Floatevery Float can be used as a quantity of Meters

Typed Tagged ... is also the result of trying to map a Checked thing

import Typed exposing (Typed, Checked, Public, tag)

type Prime
    = Prime

prime3 : Typed Checked Prime Public Int
prime3 =
    tag Prime 3

prime3
    |> Typed.map (\prime -> prime + 1) -- tee he
--: Typed Tagged Prime Public Int

→ any function that only takes Checked Prime really only receives the good stuff


type alias Checked =
Internal.Checked

Only someone with access to the tag is able to create one of those.

In effect, this means that you can only let "validated" data be of this type.

Example: ... Checked ... NaturalNumberTag Int → not every Int can be called a NumberNatural, it must be checked!

toChecked : tag -> Typed creator_ tag accessRight untyped -> Typed creatorChecked_ tag accessRight untyped

Confirm that it can be considered Checked by supplying the matching tag. Annotate or use the result as Tagged/Checked

-- module Even exposing (Even, add, multiply)


import Typed exposing (Checked, Public, Typed)

type alias Even =
    Typed Checked EvenTag Public Int

type EvenTag
    = Even

multiply : Int -> Even -> Even
multiply 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

If the tag should change after map, mapTo

oddAddOdd : Odd -> Odd -> Even
oddAddOdd oddToAdd =
    \odd ->
        odd
            |> Typed.and oddToAdd
            -- ↓ same as mapTo Even (\( o0, o1 ) -> o0 + 01)
            |> Typed.map (\( o0, o1 ) -> o0 + o1)
            |> Typed.toChecked Even

mapTo is better here since you don't have weird intermediate results like

odd
    -- : Odd
    |> Typed.and oddToAdd
    |> Typed.map (\( o0, o1 ) -> o0 + o1)
    --: Typed Tagged OddTag Public Int
    |> ...

access

access public


type alias Public =
Internal.Public

Anyone is able to access the untyped thing of a Typed ... Public by using untag

untag : Typed tag_ creator_ Public untyped -> untyped

The thing inside the Typed Public

-- module Prime exposing (Prime, n3, n5)

type alias Prime =
    Typed Checked PrimeTag Public Int

type PrimeTag
    = Prime

n3 : Prime
n3 =
    3 |> tag Prime

n5 : Prime
n5 =
    5 |> tag Prime

-- in another module using Prime
import Typed exposing (untag)

(n3 |> untag) < (n5 |> untag)
--> True

toPublic : tag -> Typed creator tag accessRight_ untyped -> Typed creator tag accessRightPublic_ untyped

Confirm that it can be considered Public by supplying the matching tag. The result can be annotated or used as Internal/Public

internal tag =
    toPublic tag >> untag

access internal


type alias Internal =
Internal

Only those with access to the tag can access the internal thing.

→ access can be limited to

This in combination with Checked hides the internals just like an opaque type

type alias ListOptimized element =
    Typed
        Checked
        ListOptimizedTag
        Internal
        { list : List element, length : Int }

type ListOptimizedTag
    = -- don't expose this variant
      ListOptimizedTag

internal : tag -> Typed creator_ tag accessRight_ untyped -> untyped

Only those who can show the tag can access the Typed Internal:

import Typed exposing (Checked, Internal, Typed)

type alias ListOptimized element =
    Typed
        Checked
        ListOptimizedTag
        Internal
        -- optimizations might change in the future
        { list : List element
        , length : Int
        }

type ListOptimizedTag
    = ListOptimized

toList =
    \listOptimized ->
        listOptimized
            |> Typed.internal ListOptimized
            |> .list

type ListOptimizedTag
    = ListOptimized

equal =
    \( listOptimizedA, listOptimizedB ) ->
        let
            a =
                listOptimizedA |> Typed.internal ListOptimized

            b =
                listOptimizedB |> Typed.internal ListOptimized
        in
        if a.length /= b.length then
            False

        else
            a.list == b.list

internal can be seen as a shortcut for toPublic, then untag

internal tag =
    Typed.toPublic tag >> Typed.untag

transform

map : (untyped -> mappedUntyped) -> Typed creator_ tag accessRight untyped -> Typed Tagged tag accessRight mappedUntyped

Change the untyped thing.

The result is not Checked, so it becomes just Tagged

import Typed exposing (Public, Tagged, Typed)

type alias Meters =
    Typed Tagged MetersTag Public Int

add1km : Meters -> Meters
add1km =
    Typed.map (\m -> m + 1000)

To confirm that the maps result is CheckedtoChecked

import Typed exposing (Checked, Public, Typed)

type alias Odd =
    Typed Checked OddTag Public Int

type OddTag
    = Odd

next : Odd -> Odd
next =
    \odd ->
        odd
            |> Typed.map (\m -> m + 2)
            |> Typed.toChecked Odd

mapToTyped : (untyped -> Typed creatorMapped tag accessRight mappedUntyped) -> Typed creator_ tag accessRight untyped -> Typed creatorMapped tag accessRight mappedUntyped

Use the untyped thing to return a Typed with the same tag & access right

-- module Cat exposing (Cat, feed)
import Typed

type alias Cat =
    Typed
        Checked
        CatTag
        Internal
        { mood : Mood, foodReserves : Float }

feed : Cat -> Cat
feed =
    Typed.map
        (\cat ->
            { cat | foodReserves = cat.foodReserves + 10 }
        )

-- in another module
import Cat
import Typed

catOnUnhappyFeed : Cat -> Cat
catOnUnhappyFeed =
    cat
        |> Typed.mapToTyped
            (\catState ->
                case catState.mood of
                    Unhappy ->
                        cat |> Cat.feed

                    Happy ->
                        cat
            )

Map multiple arguments with and

mapTo : mappedTag -> (untyped -> mappedUntyped) -> Typed creator_ tag_ Public untyped -> Typed mappedCreatorChecked_ mappedTag mappedAccessRightPublic_ mappedUntyped

map which allows specifying a tag for the mapped result.

Rights of the result can be chosen by annotating or using it as

fromMultiplyingPrimes : Prime -> Prime -> NonPrime
fromMultiplyingPrimes primeA primeB =
    (primeA |> Typed.and primeB)
        |> Typed.mapTo NonPrime (\a b -> a * b)

If the tag stays the same, just map and if necessary add toChecked

replace : untypedReplacement -> Typed creator_ tag accessRight_ untyped_ -> Typed Tagged tag accessRightPublic_ untypedReplacement

Swap out its untyped thing for a given replacement. The tag will stay the same but as with map, the creator will be set to Tagged.

Prefer over map if you need to allow the result to be Public

type SecretTag
    = Shhh

secret : Typed Checked SecretTag Internal String
secret =
    Typed.tag Shhh "dear diary"

encrypt :
    Typed Checked SecretTag Internal String
    -> Typed Checked SecretTag Internal String

secret
    |> Typed.replace "since I can't read the secret here, we can make it Public"
    --: Typed Tagged SecretTag Public String
    |> Typed.untag
--> "since I can't read the secret here, we can make it Public"

Also useful if you want to keep a tag for a different type:

type alias Vault secretTag =
    Typed Tagged secretTag Internal { encrypted : ( String, List String ) }

for
    ( Typed Checked tag Internal String
    , List (Typed Checked tag Internal String)
    )
    -> (Vault tag
       -> Vault tag
       )
for ( secretOne, secretOthers ) =
    \vault ->
        secretOne
            |> Typed.replace
                { encrypted =
                    ( secretOne |> encrypt, secretOthers |> List.map encrypt )
                }

and : Typed nextCreator_ tag accessRight nextUntyped -> Typed creator_ tag accessRight untyped -> Typed Tagged tag accessRight ( untyped, nextUntyped )

You can map, combine, ... even Internal Typeds as long as a Typed with the same tag & access rights are returned in the end

into map

-- module Prime exposing (Prime, n3, n5)


import Typed exposing (Checked, Public, Typed, mapToTyped, tag)

type alias Prime =
    Typed Checked PrimeTag Public Int

type PrimeTag
    = Prime

n3 : Prime
n3 =
    tag Prime 3

n5 : Prime
n5 =
    tag Prime 5

-- module NonPrime exposing (NonPrime)
-- import Prime exposing (Prime)
type alias NonPrime =
    Typed Checked NonPrimeTag Public Int

type NonPrimeTag
    = NonPrime

fromMultiplyingPrimes : Prime -> Prime -> NonPrime
fromMultiplyingPrimes primeA primeB =
    (primeA |> Typed.and primeB)
        |> Typed.mapTo NonPrime (\a b -> a * b)

into mapToTyped

-- module Typed.Int

smaller : Typed create tag access Int -> Typed create tag access Int -> Typed create tag access Int
smaller =
    \int0Typed int1Typed ->
        int0Typed
            |> Typed.and int1Typed
            |> Typed.mapToTyped
                (\( int0, int1 ) ->
                    if int0 <= int1 then
                        int0Typed
                    else
                        -- int1 < int0
                        int1Typed
                )

-- in another module

type OddTag
    = Odd

smaller (Typed.tag Odd 3) (Typed.tag Odd 5)
--> Typed.tag Odd 3

wrapping

A pretty specialized use-case which helps using tags you don't have access to inside your wrapper tag

Examples in the version 8 announcement

mapToWrap : mappedTagWrap -> (untyped -> mappedUntyped) -> Typed creator_ tag Public untyped -> Typed mappedCreatorChecked_ ( mappedTagWrap, tag ) mappedAccessRightPublic_ mappedUntyped

map, then put the existing tag after a given wrapper tag

type alias Hashing subject tag =
    Typed Checked tag Public (subject -> Hash)

type Each
    = Each

each :
    Hashing element elementHashTag
    -> Hashing (List element) ( Each, elementHashTag )
each elementHashing =
    Typed.mapToWrap Each
        (\elementToHash ->
            \list ->
                list |> List.map elementToHash |> Hash.sequence
        )
        elementHashing

Use wrapAnd to map multiple Typeds

This doesn't violate any rules because you have no way of getting to the wrapped tag

mapUnwrap : (untyped -> mappedUntyped) -> Typed creator_ ( tagWrap_, tagWrapped ) accessRight untyped -> Typed Tagged tagWrapped accessRight mappedUntyped

Change its untyped thing and adapt the inner, wrapped tag

The result has the same access right as before but is Tagged since we haven't verified the changed thing

-- module Secret exposing (SecretTag, secret)

type SecretTag
    = Secret

secret :
    Typed creator_ kind Public a
    -> Typed Checked ( SecretTag, kind ) Internal a
secret =
    Typed.mapToWrap Secret

encrypt :
    Typed Checked ( SecretTag, kind ) Internal String
    -> Typed Checked kind Internal { encrypted : String }
encrypt =
    Typed.mapUnwrap ...

-- for example in another module

type DiaryTag
    = Diary

diary : Typed Checked DiaryTag Internal { encrypted : String }
diary =
    secret (Typed.tag Diary "dear diary")
        --: Typed Checked ( SecretTag, DiaryTag ) Internal String
        |> Secret.encrypt

Use mapToWrap to wrap the unwrapped thing again with a different tag

wrapToChecked : tagWrap -> Typed creator_ ( tagWrap, tagWrapped ) accessRight untyped -> Typed creatorChecked_ ( tagWrap, tagWrapped ) accessRight untyped

Allow the creator to be Checked by supplying the wrapper tag

type Reverse
    = Reverse

Int.Order.increasing
    |> reverse
    --: Typed Checked ( Reverse, Int.Order.Increasing ) Public ...
    |> Typed.map ...
    --: Typed Tagged ( Reverse, Int.Order.Increasing ) Public ...
    |> Typed.wrapToChecked Reverse
    --: Typed Checked ( Reverse, Int.Order.Increasing ) Public ...

wrapToPublic : tagWrap -> Typed creator ( tagWrap, tagWrapped ) accessRight_ untyped -> Typed creator ( tagWrap, tagWrapped ) accessRightPublic_ untyped

Confirm that it can be considered Public by supplying the matching tag. The result can be annotated or used as Internal/Public

internal tag =
    toPublic tag >> untag

wrapInternal : tagWrap -> Typed creator_ ( tagWrap, tagWrapped_ ) accessRight_ untyped -> untyped

If you have a Typed Internal, its untyped thing can't be read by everyone.

If you have access to the tag, you can access its untyped thing

run :
    Typed Checked ( Hashing, thingTag ) Internal (thing -> Hash)
    -> (thing -> Hash)
run hashing =
    hashing |> Typed.wrapInternal Hashing

internal can be seen as a shortcut for wrapToPublic, then untag

internal tag =
    Typed.wrapToPublic tag >> Typed.untag

wrapAnd : Typed nextCreator_ tagFood Public nextUntyped -> Typed creator_ tagWrap accessRight untyped -> Typed Tagged ( tagWrap, tagFood ) accessRight ( untyped, nextUntyped )

and which keeps the tag from the first Typed in the chain as the wrapping tag

type alias Hashing thing tag =
    Typed Checked tag Public thing

type HashBy
    = HashBy

hashBy :
    Map mapTag (thing -> thingMapped)
    -> Hashing thingHashingTag thingMapped
    -> Hashing ( HashBy, ( mapTag, thingHashingTag ) ) thingMapped
hashBy map mappedHashing =
    map
        |> Typed.wrapAnd mappedHashing
        |> Typed.mapToWrap HashBy
            (\( change, mappedHash ) ->
                \toHash ->
                    toHash |> change |> mappedHash
            )