Describes how to turn a sequence of bytes into a nice Elm value.
decode : Decoder a -> Bytes -> Maybe a
Turn a sequence of bytes into a nice Elm value.
-- decode (unsignedInt16 BE) <0007> == Just 7
-- decode (unsignedInt16 LE) <0700> == Just 7
-- decode (unsignedInt16 BE) <0700> == Just 1792
-- decode (unsignedInt32 BE) <0700> == Nothing
The Decoder
specifies exactly how this should happen. This process may fail
if the sequence of bytes is corrupted or unexpected somehow. The examples above
show a case where there are not enough bytes.
signedInt8 : Decoder Basics.Int
Decode one byte into an integer from -128
to 127
.
signedInt16 : Bytes.Endianness -> Decoder Basics.Int
Decode two bytes into an integer from -32768
to 32767
.
signedInt32 : Bytes.Endianness -> Decoder Basics.Int
Decode four bytes into an integer from -2147483648
to 2147483647
.
unsignedInt8 : Decoder Basics.Int
Decode one byte into an integer from 0
to 255
.
unsignedInt16 : Bytes.Endianness -> Decoder Basics.Int
Decode two bytes into an integer from 0
to 65535
.
unsignedInt32 : Bytes.Endianness -> Decoder Basics.Int
Decode four bytes into an integer from 0
to 4294967295
.
float32 : Bytes.Endianness -> Decoder Basics.Float
Decode four bytes into a floating point number.
float64 : Bytes.Endianness -> Decoder Basics.Float
Decode eight bytes into a floating point number.
bytes : Basics.Int -> Decoder Bytes
Copy a given number of bytes into a new Bytes
sequence.
string : Basics.Int -> Decoder String
Decode a given number of UTF-8 bytes into a String
.
Most protocols store the width of the string right before the content, so you will probably write things like this:
import Bytes exposing (Endianness(..))
import Bytes.Decode as Decode
sizedString : Decode.Decoder String
sizedString =
Decode.unsignedInt32 BE
|> Decode.andThen Decode.string
In this case we read the width as a 32-bit unsigned integer, but you have the leeway to read the width as a Base 128 Varint for ProtoBuf, a Variable-Length Integer for SQLite, or whatever else they dream up.
map : (a -> b) -> Decoder a -> Decoder b
Transform the value produced by a decoder. If you encode negative numbers in a special way, you can say something like this:
negativeInt8 : Decoder Int
negativeInt8 =
map negate unsignedInt8
In practice you may see something like ProtoBuf’s ZigZag encoding which decreases the size of small negative numbers.
map2 : (a -> b -> result) -> Decoder a -> Decoder b -> Decoder result
Combine two decoders.
import Bytes exposing (Endiannness(..))
import Bytes.Decode as Decode
type alias Point = { x : Float, y : Float }
decoder : Decode.Decoder Point
decoder =
Decode.map2 Point
(Decode.float32 BE)
(Decode.float32 BE)
map3 : (a -> b -> c -> result) -> Decoder a -> Decoder b -> Decoder c -> Decoder result
Combine three decoders.
map4 : (a -> b -> c -> d -> result) -> Decoder a -> Decoder b -> Decoder c -> Decoder d -> Decoder result
Combine four decoders.
map5 : (a -> b -> c -> d -> e -> result) -> Decoder a -> Decoder b -> Decoder c -> Decoder d -> Decoder e -> Decoder result
Combine five decoders. If you need to combine more things, it is possible
to define more of these with map2
or andThen
.
andThen : (a -> Decoder b) -> Decoder a -> Decoder b
Decode something and then use that information to decode something else. This is most common with strings or sequences where you need to read how long the value is going to be:
import Bytes exposing (Endianness(..))
import Bytes.Decode as Decode
string : Decoder String
string =
Decode.unsignedInt32 BE
|> Decode.andThen Decode.string
Check out the docs for succeed
, fail
, and
loop
to see andThen
used in more ways!
succeed : a -> Decoder a
A decoder that always succeeds with a certain value. Maybe we are making
a Maybe
decoder:
import Bytes.Decode as Decode exposing (Decoder)
maybe : Decoder a -> Decoder (Maybe a)
maybe decoder =
let
helper n =
if n == 0 then
Decode.succeed Nothing
else
Decode.map Just decoder
in
Decode.unsignedInt8
|> Decode.andThen helper
If the first byte is 00000000
then it is Nothing
, otherwise we start
decoding the value and put it in a Just
.
fail : Decoder a
A decoder that always fails. This can be useful when using andThen
to
decode custom types:
import Bytes exposing (Endianness(..))
import Bytes.Encode as Encode
import Bytes.Decode as Decode
type Distance = Yards Float | Meters Float
toEncoder : Distance -> Encode.Encoder
toEncoder distance =
case distance of
Yards n -> Encode.sequence [ Encode.unsignedInt8 0, Encode.float32 BE n ]
Meters n -> Encode.sequence [ Encode.unsignedInt8 1, Encode.float32 BE n ]
decoder : Decode.Decoder Distance
decoder =
Decode.unsignedInt8
|> Decode.andThen pickDecoder
pickDecoder : Int -> Decode.Decoder Distance
pickDecoder tag =
case tag of
0 -> Decode.map Yards (Decode.float32 BE)
1 -> Decode.map Meters (Decode.float32 BE)
_ -> Decode.fail
The encoding chosen here uses an 8-bit unsigned integer to indicate which variant we are working with. If we are working with yards do this, if we are working with meters do that, and otherwise something went wrong!
Decide what steps to take next in your loop
.
If you are Done
, you give the result of the whole loop
. If you decide to
Loop
around again, you give a new state to work from. Maybe you need to add
an item to a list? Or maybe you need to track some information about what you
just saw?
Note: It may be helpful to learn about finite-state machines to get
a broader intuition about using state
. I.e. You may want to create a type
that describes four possible states, and then use Loop
to transition between
them as you consume characters.
loop : state -> (state -> Decoder (Step state a)) -> Decoder a
A decoder that can loop indefinitely. This can be helpful when parsing repeated structures, like a list:
import Bytes exposing (Endianness(..))
import Bytes.Decode as Decode exposing (..)
list : Decoder a -> Decoder (List a)
list decoder =
unsignedInt32 BE
|> andThen (\len -> loop (len, []) (listStep decoder))
listStep : Decoder a -> (Int, List a) -> Decoder (Step (Int, List a) (List a))
listStep decoder (n, xs) =
if n <= 0 then
succeed (Done xs)
else
map (\x -> Loop (n - 1, x :: xs)) decoder
The list
decoder first reads a 32-bit unsigned integer. That determines how
many items will be decoded. From there we use loop
to track all the
items we have parsed so far and figure out when to stop.