KeysSetlookup for multiple arbitrary keys. safe. log n
🌏 Let's build a country lookup with .code and .flag as keys
KeysSet.fromList keys
[ { flag = "🇦🇺", code = "AU", name = "Australia" }
, { flag = "🇦🇶", code = "AQ", name = "Antarctica" }
, { flag = "🇱🇧", code = "LB", name = "Lebanon" }
]
type alias Country =
{ flag : String, code : String, name : String }With a key to compare against, you can find the matching element
in log n time:
|> KeysSet.element ( keys, .flag ) "🇦🇶"
--→ Just { flag = "🇦🇶", code = "AQ", name = "Antarctica" }
|> KeysSet.element ( keys, .code ) "LB"
--→ Just { flag = "🇱🇧", code = "LB", name = "Lebanon" }
|> KeysSet.end ( keys, .code ) Down -- minimum
--→ { flag = "🇦🇶", code = "AQ", name = "Antarctica" } no MaybeWe supplied keys to construct and operate on our KeysSet,
so... Which aspects do we want it to be sorted by?
keys : Keys Country CountryKeys N2
keys =
Keys.for (\flag_ code_ -> { flag = flag_, code = code_ })
|> Keys.by ( .flag, flag )
(String.Order.earlier Char.Order.unicode)
|> Keys.by ( .code, code )
(String.Order.earlier (Char.Order.aToZ Order.tie))KeysSet holds no functions, so the Keys have to be supplied on every operation.
To ensure these given Keys are always the same for one KeysSet,
we need some boilerplate,
attaching opaque tags:
type Flag =
-- ! no exposing (..) → only constructable in this module
Flag
flag : Mapping Country Flag String
flag =
Map.tag Flag .flag
type Code
-- no exposing (..)
= Code
code : Mapping Country Code String
code =
Typed.tag Code .code
type alias CountryKeys =
-- you can just infer this
{ flag : Key Country (Order.By Flag (String.Order.Earlier Char.Order.Unicode)) N2
, code : Key Country (Order.By Code (String.Order.Earlier (Char.Order.AToZ Order.Tie))) N2
}Feels somewhat like "explicit typeclasses" :)
→ Solves problems listed in prior art alongside other goodies
🧩
when annotating a KeysSet, you'll run into types like
Emptiable (KeysSet ...) Never -> ...
-> Emptiable (KeysSet ...) never_which say: the KeysSet can never be empty
and
-> Emptiable (KeysSet ...) Possibly
Emptiable (KeysSet ...) possiblyOrNever -> ...which say: the KeysSet can possibly be empty.
emptiness-typed lets us conveniently use one API
for both non-empty and emptiable types.
the types of key counts like N2 can be found in bounded-nat. No need to understand the details; type inference has your back.
Wanna dig a bit deeper? Giving an Ordering or Mapping a unique tag is enabled by typed-value: convenient control of reading and writing for tagged things.
import KeysSet exposing (KeysSet)
import Emptiable exposing (Emptiable)
import Possibly exposing (Possibly)
import Keys exposing (Key, Keys)
import Order
import String.Order
import Char.Order
import Map exposing (Mapping)
import N exposing (N2)
type alias Operator =
{ symbol : String, name : String, kind : OperatorKind }
operatorKeys : Keys Operator OperatorKeys N2
operatorKeys =
Keys.for (\symbol_ name_ -> { symbol = symbol_, name = name_ })
|> Keys.by ( .symbol, symbol )
(String.Order.earlier Char.Order.unicode)
|> Keys.by ( .name, name )
(String.Order.earlier (Char.Order.aToZ Order.tie))
type alias OperatorKeys =
{ symbol : Key Operator (Order.By Symbol (String.Order.Earlier Char.Order.Unicode)) String N2
, name : Key Operator (Order.By Name (String.Order.Earlier Char.Order.AToZ (Order.Tie))) String N2
}
operators : Emptiable (KeysSet Operator OperatorKeys N2) never_
operators =
KeysSet.fromStack operatorKeys
(Stack.topBelow
{ symbol = ">", name = "gt", kind = Binary }
[ { symbol = "<", name = "lt", kind = Binary }
, { symbol = "==", name = "eq", kind = Binary }
, { symbol = "-", name = "negate", kind = Unary }
]
)
nameOfOperatorSymbol : String -> Emptiable String Possibly
nameOfOperatorSymbol operatorSymbol =
operators
|> KeysSet.element ( operatorKeys, .symbol ) operatorSymbol
type Name
= Name
name : Mapping Operator Name String
name =
Map.tag Name .name
type Symbol
= Symbol
symbol : Mapping Operator Symbol String
symbol =
Map.tag Symbol .symboltype alias ConversationStep =
{ youSay : String, answer : String }
type alias ByYouSay =
{ youSay : Key ConversationStep (Order.By YouSay (String.Order.Earlier (Char.Order.AToZ Order.Tie))) String N1 }
youSayKey : Keys ConversationStep ByYouSay N1
youSayKey =
Keys.for (\youSay_ -> { youSay = youSay_ })
|> Keys.by ( .youSay, youSay )
(String.Order.earlier (Char.Order.aToZ Order.tie))
answers : Emptiable (KeysSet ConversationStep ByYouSay N1) Possibly
answers =
KeysSet.fromList youSayKey
[ { youSay = "Hi"
, answer = "Hi there!"
}
, { youSay = "Bye"
, answer = "Ok, have a nice day and spread some love."
}
, { youSay = "How are you"
, answer = "I don't have feelings :("
}
, { youSay = "Are you a robot"
, answer = "I think the most human answer is 'Haha... yes'"
}
]import Emptiable exposing (Emptiable)
import Stack
import KeysSet exposing (KeysSet)
import N exposing (N2)
import User exposing (User(..))
exampleUsers : Emptiable (KeysSet User User.Keys N2) never_
exampleUsers =
KeySet.fromStack User.keys
(Stack.topBelow
(User { name = "Fred", email = [email protected].. })
[ User { name = "Ann", email = [email protected].. }
, User { name = "Annother", email = [email protected].. }
, User { name = "Bright", email = [email protected].. }
]
)
exampleUsers |> KeySet.size
--→ 3
exampleUsers |> KeySet.element User.keys [email protected]..
--→ Emptiable.filled { name = "Ann", email = [email protected].. }
-- module User exposing (User(..), Keys, keys)
import KeySet
import Keys
import Order
import String.Order
import Char.Order
import Map exposing (Mapping)
import N exposing (N2)
import Email
type User
= User
{ email : Email
, name : String
, settings : Settings
}
type EmailTag
= Email
email : Mapping User EmailTag Email
email =
Map.tag Email (\(User userData) -> userData.email)
type NameTag
= Name
name : Mapping User NameTag String
name =
Map.tag Name (\(User userData) -> userData.name)
keys : Keys.Keys User Keys N2
keys =
Keys.for (\email_ name_ -> { email = email_, name = name_ })
|> Keys.by ( .email, email ) Email.byHostFirst
|> Keys.by ( .name, name )
(String.Order.earlier (Char.Order.aToZ Order.tie))
type alias Keys =
{ email : Key User (Order.By EmailTag Email.ByHostFirst) Email N2
, name : Key User (Order.By NameTag (String.Order.Earlier (Char.Order.AToZ Order.Tie))) String N2
}
-- module Email exposing (Email, byHostFirst, ByHostFirst)
type alias Email =
{ host : String, label : String }
type alias ByHostFirst =
Order.OnTieNext
(Order.By Email.HostTag (String.Order.Earlier (Char.Order.AToZ Order.Tie)))
(Order.By Email.LabelTag (String.Order.Earlier (Char.Order.AToZ Order.Tie)))
byHostFirst : Ordering Email ByHostFirst
byHostFirst =
Order.by Email.host
(String.Order.earlier (Char.Order.aToZ Order.tie))
|> Order.onTie
(Order.by Email.label
(String.Order.earlier (Char.Order.aToZ Order.tie))
)import KeysSet exposing (KeysSet)
import Emptiable exposing (Emptiable)
import Possibly exposing (Possibly)
import N exposing (N2)
type alias State =
{ users : Emptiable (KeysSet User UserKeys N2) Possibly
, activeUserName : String
}
initialState : State
initialState =
{ users = exampleUsers }
reactTo event =
case event of
Registered { name, email } ->
\state ->
case state.users |> KeysSet.element ( User.keys, .name ) name of
Emptiable.Filled _ ->
-- name taken already
Emptiable.Empty _ ->
case state.users |> KeysSet.element ( User.keys, .email ) email of
Emptiable.Filled _ ->
-- email taken already
Emptiable.Empty _ ->
{ state
| users =
state.users
|> KeysSet.insert User.keys
{ name = name
, email = email
, settings = defaultSettings
}
}
SettingsChanged settingsChange ->
\state ->
{ state
| users =
state.users
|> KeysSet.elementAlter
( .name, state.activeUserName )
(applySettingsChange settingsChange)
}
UserSwitched name ->
\state -> { state | activeUserName = name }partnerKeys =
Keys.for
(\partner_ partnerOfPartner_ ->
{ partner = partner_, partnerOfPartner = partnerOfPartner_ }
)
|> Keys.by ( .partner, partner )
(String.Order...)
|> Keys.by ( .partnerOfPartner, partnerOfPartner )
(String.Order...)
partners =
KeysSet.fromList partnerKeys
[ { partner = "Ann", partnerOfPartner = "Alan" }
, { partner = "Alex", partnerOfPartner = "Alistair" }
, { partner = "Alan", partnerOfPartner = "Ann" }
-- wait, this is no duplicate and is inserted
]A KeysSet ony makes sense when the keys describe something different
Maybe take a look at graphs or elm-bidict instead.
log n keysOrdering key = ... key, key -> Ordercomparablekey -> StringcomparableKeyOrder.reverseelement -> key function as part of a given KeyKeys are the sameKeySet.minimum, maximum, foldFromOne, fold don't need Maybeallowable-stateemptiness-typedcomparableKeyDict wrapper when its key contains a custom type.
Often more a hindrance than helpfulcomparable or k -> k -> Order)union, intersection, ...)key -> key -> OrderOrder's prior artcomparable keys. Therefore simpler while not relying on magic... -> comparablekey -> StringtoString implementation not returning a unique String for all keystoString needs to do heavy workedkelly303/elm-any-type-collections Any.Set, Any.Dict with a toComparable functionminiBill/elm-generic-dict with a toComparable functioninsert from the wrong API "instance" with a different function is still possible but less likely to happen in practicekey -> Maybe value instead of a data structure>= n runtime== among other things will failclear to only remove all elements but keep the functionn runtimeKeySet butcomparable