Bytes.Decode
Functions for creating things from a sequence of bytes.
Describes how to turn a sequence of bytes into a nice Gren value.
Turn a sequence of bytes into a nice Gren 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.
Integers
Decode one byte into an integer from -128
to 127
.
Decode two bytes into an integer from -32768
to 32767
.
Decode four bytes into an integer from -2147483648
to 2147483647
.
Decode one byte into an integer from 0
to 255
.
Decode two bytes into an integer from 0
to 65535
.
Decode four bytes into an integer from 0
to 4294967295
.
Floats
Decode four bytes into a floating point number.
Decode eight bytes into a floating point number.
Bytes
Copy a given number of bytes into a new Bytes
sequence.
Strings
Decode a given number of UTF-8 bytes into a String
.
Most protocols store the length 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 length as a 32-bit unsigned integer, but you have the leeway to read the length as a Base 128 Varint for ProtoBuf, a Variable-Length Integer for SQLite, or whatever else they dream up.
Map
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.
Combine two decoders.
import Bytes exposing ( Endianness(..) )
import Bytes.Decode as Decode
type alias Point =
{ x : Float
, y : Float
}
makePoint : Float -> Float -> Point
makePoint x y =
{ x = x
, y = y
}
decoder : Decode.Decoder Point
decoder =
Decode.map2 makePoint (Decode.float32 BE) (Decode.float32 BE)
Combine three decoders.
Combine four decoders.
Combine five decoders. If you need to combine more things, it is possible
to define more of these with map2
or andThen
.
And Then
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!
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
.
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 =
when distance is
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 =
when tag is
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!
Loop
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 an array? 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.
A decoder that can loop indefinitely. This can be helpful when parsing repeated structures, like an array:
import Bytes exposing (Endianness(..))
import Bytes.Decode as Decode exposing (..)
array : Decoder a -> Decoder (Array a)
array decoder =
unsignedInt32 BE
|> andThen (\len -> loop (len, []) (arrayStep decoder))
arrayStep : Decoder a -> { fst : Int, snd : Array a } -> Decoder (Step { fst Int, snd : Array a } (Array a))
arrayStep decoder (n, xs) =
if n <= 0 then
succeed (Done xs)
else
map (\x -> Loop (n - 1, x :: xs)) decoder
The array
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.