lue-bird / elm-emptiness-typed / Emptiable

📦 A Maybe value that can be made non-empty depending on what we know – an "emptiable-able" value

in arguments

import Stack exposing (Stacked)

fill : Emptiable fill Never -> fill

top : Emptiable (Stacked element) Never -> element

in type declarations

import Emptiable exposing (Emptiable)
import Stack exposing (Stacked)

type alias Model =
    WithoutConstructorFunction
        { searchKeyWords : Emptiable (Stacked String) Never
        }

where RecordWithoutConstructorFunction stops the compiler from creating a constructor function for Model


type Emptiable fill possiblyOrNever
    = Empty possiblyOrNever
    | Filled fill

📦 Like Maybe, but able to know at type-level whether Empty is a possibility

import Emptiable exposing (Emptiable, filled, fill)

[ filled 1, filled 7 ]
    --: List (Emptiable number_ never_)
    |> List.map fill
--> [ 1, 7 ]

Emptiable by itself probably won't be that useful, but it can make data structures type-safely non-emptiable:

import Emptiable exposing (map)

top : Emptiable (Stacked element) Never -> element

map Dict.NonEmpty.head
--: Emptiable (NonEmptyDict comparable v) possiblyOrNever
--: -> Emptiable ( comparable, v ) possiblyOrNever

Go take a look at all the data structures in this package

create

empty : Emptiable filling_ Possibly

Insert joke about life here

Emptiable.empty
    |> Emptiable.map (\x -> x / 0)
--> Emptiable.empty

filled : fill -> Emptiable fill never_

Emptiable that certainly exists, allowing type-safe extraction

import Emptiable exposing (filled, fill)

filled "Bami" |> fill
--> "Bami"

fromMaybe : Maybe value -> Emptiable value Possibly

Convert a Maybe to an Emptiable Possibly

To create new Emptiables, use filled and empty instead!

fuzz : Fuzzer fill -> Fuzzer (Emptiable fill Possibly)

Emptiable fill Possibly Fuzzer of a given fill

import Fuzz

Emptiable.fuzz Fuzz.char
    |> Fuzz.examples 4
--> [ filled 'U', empty, filled 'M', empty ]

transform

map : (fill -> mapped) -> Emptiable fill possiblyOrNever -> Emptiable mapped possiblyOrNever

If the Emptiable is filled, change it based on its current fill:

import Emptiable exposing (filled, map)

filled -3 |> map abs
--> filled 3

Emptiable.empty |> map abs
--> Emptiable.empty

mapFlat : (fill -> Emptiable fillIfBothFilled possiblyOrNever) -> Emptiable fill possiblyOrNever -> Emptiable fillIfBothFilled possiblyOrNever

Chain together operations that may return empty. It's like calling map|>flatten:

If the argument is Never empty, a given function takes its fill and returns a new possibly empty value

Some call it andThen or flatMap

import Emptiable exposing (Emptiable, mapFlat)

emptiableString
    |> mapFlat parse
    |> mapFlat extraValidation

parse : ( Char, String ) -> Emptiable Parsed Possibly
extraValidation : Parsed -> Emptiable Parsed Possibly

For any number of arguments: and... |> ... |>mapFlat:

import Emptiable exposing (filled, mapFlat, and)

(filled 3)
    |> and (filled 4)
    |> and (filled 5)
    |> mapFlat
        (\( ( a, b ), c ) -> filled ( a, b, c ))
--> filled ( 3, 4, 5 )

and : Emptiable anotherFill possiblyOrNever -> Emptiable fill possiblyOrNever -> Emptiable ( fill, anotherFill ) possiblyOrNever

If the incoming food and the given argument are filled, give a filled tuple of both fills back

If any is empty, give a Emptiable.empty back

and comes in handy when multiple arguments need to be filled

import Emptiable exposing (filled, map, and)

filled 3
    |> and (filled 4)
    |> and (filled 5)
    |> map (\( ( a0, a1 ), a2 ) -> a0^a1 - a2^2)
--> filled 56

flatten : Emptiable (Emptiable fill possiblyOrNever) possiblyOrNever -> Emptiable fill possiblyOrNever

In a nestable Emptiable: Only keep it filled if the inner Emptiable is filled

Some call it join

import Emptiable exposing (filled)

filled (filled 1) |> Emptiable.flatten
--> filled 1

filled Emptiable.empty |> Emptiable.flatten
--> Emptiable.empty

Emptiable.empty |> Emptiable.flatten
--> Emptiable.empty

fill : Emptiable fill Basics.Never -> fill

Safely extract the filled content from a Emptiable fill Never

import Emptiable exposing (filled)

filled (filled (filled "Bami"))
    |> Emptiable.fill
    |> Emptiable.fill
    |> Emptiable.fill
--> "Bami"

first : Empty ( first, others_ ) Never -> first
first =
    Emptiable.fill >> Tuple.first

fillElseOnEmpty : (possiblyOrNever -> fill) -> Emptiable fill possiblyOrNever -> fill

Lazily use a fallback value if the Emptiable is empty

import Possibly exposing (Possibly(..))
import Emptiable exposing (Emptiable(..), fillElseOnEmpty)
import Dict

Dict.empty
    |> Dict.get "Hannah"
    |> Emptiable.fromMaybe
    |> fillElseOnEmpty (\_ -> "unknown")
--> "unknown"

fill =
    fillElseOnEmpty never

fatten =
    fillElseOnEmpty

toMaybe : Emptiable fill possiblyOrNever_ -> Maybe fill

Convert to a Maybe

Don't try to use this prematurely. Keeping type information as long as possible is always a win

type-level

emptyAdapt : (possiblyOrNever -> adaptedPossiblyOrNever) -> Emptiable fill possiblyOrNever -> Emptiable fill adaptedPossiblyOrNever

Change the possiblyOrNever type

Returning a type declaration value or argument that is Empty Possibly

An Empty possiblyOrNever can't be used as Empty Possibly

import Emptiable
import Possibly exposing (Possibly)
import Stack exposing (Stacked)

type alias Log =
    Empty Possibly (Stacked String)

fromStack : Emptiable (Stacked String) possiblyOrNever_ -> Log
fromStack stackFilled =
    stackFilled
        --: `possiblyOrNever_` but we need `Possibly`
        |> Emptiable.emptyAdapt (always Possible)

An argument or a type declaration value is Never

The Never can't be unified with Possibly or a type variable

import Emptiable
import Stack exposing (Stacked)

theShorter :
    Emptiable (Stacked element) Never
    -> Emptiable (Stacked element) possiblyOrNever
    -> Emptiable (Stacked element) possiblyOrNever
theShorter aStack bStack =
    if Stack.length bStack > Stack.length aStack then
        bStack

    else
        aStack
            --: `Never` but we need `possiblyOrNever`
            |> Emptiable.emptyAdapt never

makes both branches return possiblyOrNever