Language Guide - Scala

Installation

Prerequisites:

  • Stack

  • Alex, which can be installed with stack install alex

  • Happy, which can be installed with stack install happy

The Scala target compiler is located in directory lang/scala, type stack install lang/scala. The compiler consists of only one executable, you can put it anywhere you want.

Usage

Usage: gugugu-scala (-i|--input INPUT) (-o|--output OUTPUT)
                    (-p|--package-prefix PREFIX)
                    [-t|--field-transformer ARG] [--with-codec]
                    [--with-client] [--with-server]

Available options:
  -i,--input INPUT         the directory containing the definition files
  -o,--output OUTPUT       the directory to put the generated sources
  -p,--package-prefix PREFIX
                           the package prefix, e.g. com.example.foo.generated
  -t,--field-transformer ARG
                           transformer for field names. Possible values are:
                               id    no transformation, default
                               snake camelCase to snake_case
  --with-codec             pass this flag to generate codecs, default to false
  --with-client            pass this flag to generate client, default to false
  --with-server            pass this flag to generate server, default to false
  -h,--help                Show this help text

Module

Gugugu module is represented by Scala packages, with module name lower-cased, without underscores.

Type Map

Primitives

Gugugu Type

Scala Type

Unit

Unit

Bool

Boolean

Int32

Int

Int64

Long

Double

Double

String

String

Maybe A

Option[A]

List A

Vector[A]

Record Type

Record type are represented by case class.

data Book
  = Book
    { id   :: Int64
    , name :: String
    }

becomes

case class Book
    ( id   : Long
    , name : String
    )

Enum Type

Enum type are represented by sealed trait.

data Color
  = Red
  | Green
  | Blue

becomes

sealed trait Color

object Color {
  case object Red   extends Color
  case object Green extends Color
  case object Blue  extends Color
}

Encoder and Decoder

The generated codecs have type

package gugugu

trait Encoder[a] {
  def encode[s, r](s: s, a: a, impl: EncoderImpl[s, r]): s
}

trait Decoder[a] {
  def decode[s, r](s: s, impl: DecoderImpl[s, r]): (s, a)
}

object Encoder {
  def apply[a](implicit encoder: Encoder[a]): Encoder[a]
  def encode[s, r, a](a: a, impl: EncoderImpl[s, r])(implicit encoder: Encoder[a]): r
}

object Decoder {
  def apply[a](implicit decoder: Decoder[a]): Decoder[a]
  def decode[s, r, a](buf: r, impl: DecoderImpl[s, r])(implicit decoder: Decoder[a]): a
}

The encoder and decoder are always defined as implicits of the companion object, so you can always get them with the expression Encoder[a] or Decoder[a].

The EncoderImpl[s, r] and DecoderImpl[s, r] is two values you have to provide to describe you do you encode the.

Use the Encoder.encode[s, r, a] to encode a value of type a to type r, with the encoder and the EncoderImpl[s, r]. Likewise, ese the Decoder.decode[s, r, a] to decode a value of type a from type r, with the decoder and the DecoderImpl[s, r].

The codec are polymorphic over s and r, with the EncoderImpl/DecoderImpl provided, you can encode/decode values to/from any type you want.

The EncoderImpl/DecoderImpl are defined as.

package gugugu

trait EncoderImpl[s, r] {
  def initializeState(): s
  def encodeState(s: s): r

  def encodeUnit(s: s, v: Unit): s
  def encodeBool(s: s, v: Boolean): s
  def encodeInt32(s: s, v: Int): s
  def encodeInt64(s: s, v: Long): s
  def encodeDouble(s: s, v: Double): s
  def encodeString(s: s, v: String): s

  def encodeMaybe(s: s, isNothing: Boolean, k: s => s): s

  def encodeList(s: s, len: Int, k: s => s): s
  def encodeListNth(s: s, i: Int, k: s => s): s

  def encodeRecord(s: s, nFields: Int, k: s => s): s
  def encodeRecordField( s: s, i: Int
                       , name: String
                       , k: s => s
                       ): s

  def encodeEnum[a]( s: s
                   , indexMap: Map[a, Int], nameMap: Map[a, String]
                   , v: a): s
}

trait DecoderImpl[s, r] {
  def decodeState(r: r): s
  def finalizeState(s: s): Unit

  def decodeUnit(s: s): (s, Unit)
  def decodeBool(s: s): (s, Boolean)
  def decodeInt32(s: s): (s, Int)
  def decodeInt64(s: s): (s, Long)
  def decodeDouble(s: s): (s, Double)
  def decodeString(s: s): (s, String)

  def decodeMaybe[a](s: s, k: (s, Boolean) => (s, a)): (s, a)

  def decodeList[a](s: s, k: (s, Int) => (s, a)): (s, a)
  def decodeListNth[a](s: s, i: Int, k: s => (s, a)): (s, a)

  def decodeRecord[a](s: s, nFields: Int, k: s => (s, a)): (s, a)
  def decodeRecordField[a]( s: s, i: Int
                          , name: String
                          , k: s => (s, a)
                          ): (s, a)

  def decodeEnum[a]( s: s
                   , indexMap: Map[Int, a], nameMap: Map[String, a]
                   ): (s, a)

}

The type variable s is the intermediate state of the encoder/decoder, and the r is the final representation of the serialized type. The encoder/decoder state is initialized by initializeState/decodeState, and finalized by encodeState/finalizeState.

Every function of the EncoderImpl/DecoderImpl transforms the state, it is up to you to use immutable state or mutable state.

Please consult the example located in examples/impls/scala/src/main/scala/guguguexamples/common/JsonCodecImpl.scala to write a EncoderImpl/DecoderImpl.