Генерация рекурсивных структур в scalacheck

Я пытаюсь создать генератор для рекурсивного типа данных с именем Row. Строка представляет собой список именованных Val, где Val является либо атомарным Bin, либо вложенным Row.

Это мой код:

package com.dtci.data.anonymize.parquet

import java.nio.charset.StandardCharsets
import org.scalacheck.Gen

object TestApp extends App {

  sealed trait Val
  case class Bin(bytes: Array[Byte]) extends Val
  object Bin {
    def from_string(str: String): Bin = Bin(str.getBytes(StandardCharsets.UTF_8))
  }
  case class Row(flds: List[(String, Val)]) extends Val

  val gen_bin = Gen.alphaStr.map(Bin.from_string)
  val gen_field_name = Gen.alphaLowerStr
  val gen_field = Gen.zip(gen_field_name, gen_val)
  val gen_row = Gen.nonEmptyListOf(gen_field).map(Row.apply)
  def gen_val: Gen[Val] = Gen.oneOf(gen_bin, gen_row)

  gen_row.sample.get.flds.foreach( fld => println(s"${fld._1} --> ${fld._2}"))
}

Он падает со следующей трассировкой стека:

Exception in thread "main" java.lang.NullPointerException
    at org.scalacheck.Gen.$anonfun$flatMap$2(Gen.scala:84)
    at org.scalacheck.Gen$R.flatMap(Gen.scala:243)
    at org.scalacheck.Gen$R.flatMap$(Gen.scala:240)
    at org.scalacheck.Gen$R$$anon$3.flatMap(Gen.scala:228)
    at org.scalacheck.Gen.$anonfun$flatMap$1(Gen.scala:84)
    at org.scalacheck.Gen$Parameters.useInitialSeed(Gen.scala:318)
    at org.scalacheck.Gen$$anon$5.doApply(Gen.scala:255)
    at org.scalacheck.Gen$$anon$1.$anonfun$doApply$1(Gen.scala:110)
    at org.scalacheck.Gen$Parameters.useInitialSeed(Gen.scala:318)
    at org.scalacheck.Gen$$anon$1.doApply(Gen.scala:109)
    at org.scalacheck.Gen.$anonfun$map$1(Gen.scala:79)
    at org.scalacheck.Gen$Parameters.useInitialSeed(Gen.scala:318)
    at org.scalacheck.Gen$$anon$5.doApply(Gen.scala:255)
    at org.scalacheck.Gen.$anonfun$flatMap$2(Gen.scala:84)
    at org.scalacheck.Gen$R.flatMap(Gen.scala:243)
    at org.scalacheck.Gen$R.flatMap$(Gen.scala:240)
    at org.scalacheck.Gen$R$$anon$3.flatMap(Gen.scala:228)
    at org.scalacheck.Gen.$anonfun$flatMap$1(Gen.scala:84)
    at org.scalacheck.Gen$Parameters.useInitialSeed(Gen.scala:318)
    at org.scalacheck.Gen$$anon$5.doApply(Gen.scala:255)
    at org.scalacheck.Gen$$anon$1.$anonfun$doApply$1(Gen.scala:110)
    at org.scalacheck.Gen$Parameters.useInitialSeed(Gen.scala:318)
    at org.scalacheck.Gen$$anon$1.doApply(Gen.scala:109)
    at org.scalacheck.Gen$.$anonfun$sequence$2(Gen.scala:492)
    at scala.collection.LinearSeqOps.foldLeft(LinearSeq.scala:168)
    at scala.collection.LinearSeqOps.foldLeft$(LinearSeq.scala:164)
    at scala.collection.immutable.List.foldLeft(List.scala:79)
    at org.scalacheck.Gen$.$anonfun$sequence$1(Gen.scala:490)
    at org.scalacheck.Gen$Parameters.useInitialSeed(Gen.scala:318)
    at org.scalacheck.Gen$$anon$5.doApply(Gen.scala:255)
    at org.scalacheck.Gen.$anonfun$map$1(Gen.scala:79)
    at org.scalacheck.Gen$Parameters.useInitialSeed(Gen.scala:318)
    at org.scalacheck.Gen$$anon$5.doApply(Gen.scala:255)
    at org.scalacheck.Gen$$anon$1.$anonfun$doApply$1(Gen.scala:110)
    at org.scalacheck.Gen$Parameters.useInitialSeed(Gen.scala:318)
    at org.scalacheck.Gen$$anon$1.doApply(Gen.scala:109)
    at org.scalacheck.Gen.$anonfun$flatMap$2(Gen.scala:84)
    at org.scalacheck.Gen$R.flatMap(Gen.scala:243)
    at org.scalacheck.Gen$R.flatMap$(Gen.scala:240)
    at org.scalacheck.Gen$R$$anon$3.flatMap(Gen.scala:228)
    at org.scalacheck.Gen.$anonfun$flatMap$1(Gen.scala:84)
    at org.scalacheck.Gen$Parameters.useInitialSeed(Gen.scala:318)
    at org.scalacheck.Gen$$anon$5.doApply(Gen.scala:255)
    at org.scalacheck.Gen$.$anonfun$sized$1(Gen.scala:551)
    at org.scalacheck.Gen$Parameters.useInitialSeed(Gen.scala:318)
    at org.scalacheck.Gen$$anon$5.doApply(Gen.scala:255)
    at org.scalacheck.Gen$$anon$1.$anonfun$doApply$1(Gen.scala:110)
    at org.scalacheck.Gen$Parameters.useInitialSeed(Gen.scala:318)
    at org.scalacheck.Gen$$anon$1.doApply(Gen.scala:109)
    at org.scalacheck.Gen.$anonfun$map$1(Gen.scala:79)
    at org.scalacheck.Gen$Parameters.useInitialSeed(Gen.scala:318)
    at org.scalacheck.Gen$$anon$5.doApply(Gen.scala:255)
    at org.scalacheck.Gen.sample(Gen.scala:154)

Что не так с моим кодом, и как мне было бы лучше всего самому его диагностировать?

В качестве примечания, я видел замечания о том, что Gen.oneOf является строгим и нуждается в Gen.lzy для рекурсивных структур. Но если в моем коде я оберну определение gen_val внутри Gen.lzy(...), то я получу переполнение стека, а не текущее исключение нулевого указателя.


person mac01021    schedule 12.07.2020    source источник


Ответы (1)


Прежде всего, будьте осторожны при использовании object Main extends App. Я нахожу семантику инициализации его полей менее очевидной, чем старый добрый main с семантикой строки за строкой:

object Main {
  def main(args: Array[String]): Unit = {...}
}

Вероятно, это проблема с NullPointerException.

Обычно это можно исправить, внимательно проверив порядок инициализации полей и пометив некоторые (или все) val как lazy.

StackOverflowError возникает из-за слишком глубокой генерируемой структуры данных.

Как правило, когда вы имеете дело с рекурсией любого типа, всегда учитывайте базовый случай, когда рекурсия должна остановиться, и шаг, который в конечном итоге приведет к базовому случаю< /эм>.

В вашем конкретном случае мы можем использовать Gen.sized и Gen.resize, которые отвечают за размер генерируемых элементов (дополнительную информацию см. в документации и Google).

package com.dtci.data.anonymize.parquet

import java.nio.charset.StandardCharsets
import org.scalacheck.Gen

object Main extends App {

  sealed trait Val
  case class Bin(bytes: Array[Byte]) extends Val
  object Bin {
    def from_string(str: String): Bin = Bin(str.getBytes(StandardCharsets.UTF_8))
  }
  case class Row(flds: List[(String, Val)]) extends Val

  val gen_bin = Gen.alphaStr.map(Bin.from_string)
  val gen_field_name = Gen.alphaLowerStr
  val gen_field = Gen.zip(gen_field_name, gen_val)
  val gen_row = Gen.sized(size => Gen.resize(size / 2, Gen.nonEmptyListOf(gen_field).map(Row.apply)))

  def gen_val: Gen[Val] = Gen.sized { size =>
    if (size <= 0) {
      gen_bin
    } else {
      Gen.oneOf(gen_bin, gen_row)
    }
  }

  gen_row.sample.get.flds.foreach(fld => println(s"${fld._1} --> ${fld._2}"))
}
person Igor Ramazanov    schedule 14.07.2020