Constant codecs are codecs that always encode a specific bit pattern. For example, the constant(bin"1110")
always encodes the bit pattern 1110
. When decoding, a constant codec consumes the same number of bits as its constant value, and then either validates that the consumed bits match the constant value or ignores the consumed bits. Codecs created by constant(...)
perform validation when decoding and codecs created by constantLenient(...)
ignore the consumed bits.
For example, decoding 0000
with each technique yields:
scala> val x = constant(bin"1110").decode(bin"0000")
x: scodec.Attempt[scodec.DecodeResult[Unit]] =
Failure(expected constant BitVector(4 bits, 0xe) but got BitVector(4 bits, 0x0))
scala> val y = constantLenient(bin"1110").decode(bin"0000")
y: scodec.Attempt[scodec.DecodeResult[Unit]] =
Successful(DecodeResult((),BitVector(empty)))
The constant(...)
method returns a Codec[Unit]
, which may seem odd at first glance. Unit codecs occur frequently in scodec. They are used for a variety of use cases, including encoding a predefined value, decoding a specific pattern, manipulating the remainder during decoding, or raising errors from encoding/decoding.
When working with binary formats that make heavy use of constant values, manually wrapping each constant bit pattern with a constant
method can be verbose. This verbosity can be avoided by importing implicit conversions that allow treating binary literals as codecs.
scala> import scodec.codecs.literals._
import scodec.codecs.literals._
scala> val c: Codec[Unit] = bin"1110"
c: scodec.Codec[Unit] = constant(BitVector(4 bits, 0xe))
Any codec can be turned in to a unit codec — that is, a Codec[Unit]
— using the unit
method on the Codec
type. The resulting codec encodes and decodes using the original codec, but the decoded value is thrown away, and the value to encode is “fixed” at tht time the unit codec is generated.
For example:
scala> val c = int8.unit(-1)
c: scodec.Codec[Unit] = ...
scala> val enc = c.encode(())
enc: scodec.Attempt[scodec.bits.BitVector] =
Successful(BitVector(8 bits, 0xff))
scala> val dec = c.decode(bin"00000000 00000001")
dec: scodec.Attempt[scodec.DecodeResult[Unit]] =
Successful(DecodeResult((),BitVector(8 bits, 0x01)))
In this example, the value to encode is fixed to -1
at the time c
is created. Hence, every call to encode results in a call to int8.encode(-1)
. Decoding is interesting in that it consumed 8 bits of the vector.
In general, converting a codec to a unit codec may seem like a useless operation. However, it plays an important role in both tuple codecs and heterogeneous list codecs, which are both covered at length later.
Recall that scodec.Err
includes a context stack along with a message and any error type specific data. The errors generated by the built-in codecs typically do not provide context in errors. Rather, context can be provided using the withContext
method on Codec
.
For example:
scala> val noContext = int8.decode(bin"1111")
noContext: scodec.Attempt[scodec.DecodeResult[Int]] =
Failure(cannot acquire 8 bits from a vector that contains 4 bits)
scala> val oneContext = int8.withContext("x").decode(bin"1111")
oneContext: scodec.Attempt[scodec.DecodeResult[Int]] =
Failure(x: cannot acquire 8 bits from a vector that contains 4 bits)
scala> val twoContext = int8.withContext("x").withContext("y").decode(bin"1111")
twoContext: scodec.Attempt[scodec.DecodeResult[Int]] =
Failure(y/x: cannot acquire 8 bits from a vector that contains 4 bits)
Each of these examples fails with the same error, with the exception of the context stack.
Context stacking is typically used to provide the path in to a large structure, in the same way that lenses peer in to a large structure.
The |
operator on a String
is provided as an alias for withContext
with the arguments reversed — e.g., "x" | codec
is equivalent to codec.withContext("x")
. In standalone examples, withContext
is typically clearer but in large structures, where each field in a record is labelled with its name, the |
syntax may be clearer.
There are a number of other miscellaneous combinators that are useful.
TODO - complete, compact, withToString