Как рекурсивно использовать генераторы FsCheck?

Я использую FsCheck для тестирования на основе свойств, поэтому я определил набор генераторов для пользовательских типов. Одни типы состоят из других, и для всех есть генераторы. Определив генератор для буквенно-цифрового типа, я хочу определить генератор для типа RelativeUrl, а RelativeUrl представляет собой список из 1-9 буквенно-цифровых значений, разделенных косой чертой. Вот определение, которое работает (Alpanumeric имеет свойство «Value», которое преобразует его в String):

static member RelativeUrl() =
    Gen.listOfLength (System.Random().Next(1, 10)) <| Generators.Alphanumeric()
    |> Gen.map (fun list -> String.Join("/", list |> List.map (fun x -> x.Value)) |> RelativeUrl)

Несмотря на то, что это довольно просто, мне не нравится, что я использую метод Random.Next вместо использования генераторов случайных чисел FsCheck. Поэтому я попытался переопределить его следующим образом:

static member RelativeUrl_1() =
    Arb.generate<byte> 
    |> Gen.map int 
    |> Gen.suchThat (fun x -> x > 0 && x <= 10)
    |> Gen.map (fun length -> Gen.listOfLength length <| Generators.Alphanumeric())
    |> Gen.map (fun list -> String.Join("/", list))

Компилятор принимает это, но на самом деле это неправильно: «список» в последнем выражении — это не список буквенно-цифровых значений, а Gen. Следующая попытка:

static member RelativeUrl() =
    Arb.generate<byte> 
    |> Gen.map int 
    |> Gen.suchThat (fun x -> x > 0 && x <= 10)
    |> Gen.map (fun length -> Gen.listOfLength length <| Generators.Alphanumeric())
    |> Gen.map (fun list -> list |> Gen.map (fun elem -> String.Join("/", elem |> List.map (fun x -> x.Value))  |> RelativeUrl))

Но это тоже не работает: я получаю Gen of Gen of RelativeUrl, а не Gen of RelativeUrl. Так как же правильно комбинировать генераторы на разных уровнях?


person Vagif Abilov    schedule 31.03.2016    source источник
comment
Вместо System.Random нельзя использовать Gen.choose?   -  person Mark Seemann    schedule 31.03.2016
comment
@MarkSeemann, вы можете использовать choose вместо Random, мгновенно сэмплируя его, но это противоречит цели, потому что этот choose не будет частью результирующего генератора, а будет работать как своего рода служебная функция. Не лучше, чем Random, правда.   -  person Fyodor Soikin    schedule 31.03.2016
comment
@FyodorSoikin Я не согласен - вы никогда не захотите использовать System.Random в генераторах, потому что это разрушит воспроизводимость (т. Е. Одно и то же семя каждый раз возвращает разные результаты) и сделает сжатие невозможным (или, по крайней мере, недетерминированным).   -  person Kurt Schelfthout    schedule 01.04.2016
comment
@KurtSchelftout: Марк предложил использовать Gen.choose вместо Random. Если вы подключите его вместо Random в первом блоке кода OP, это будет бесполезно, не так ли?   -  person Fyodor Soikin    schedule 01.04.2016
comment
@FyodorSoikin Просто прочитайте свой комментарий еще раз - да, если вы используете Gen.choose |> Gen.sample, это не лучше, чем Random. Но не думайте, что @MarkSeemann имел в виду именно это :) Путаница повсюду... но я думаю, что мы все говорим об одном и том же.   -  person Kurt Schelfthout    schedule 01.04.2016


Ответы (2)


Gen.map имеет сигнатуру (f: 'a -> 'b) -> Gen<'a> -> Gen<'b>, то есть принимает функцию от 'a до 'b, затем Gen<'a> и возвращает Gen<'b>. Можно думать об этом как о «применении» данной функции к тому, что находится «внутри» данного генератора.

Но функция, которую вы предоставляете в своем вызове map, на самом деле является int -> Gen<Alphanumeric list>, то есть она возвращает не какое-то 'b, а точнее Gen<'b>, так что результатом всего выражения становится Gen<Gen<Alphanumeric list>>. Вот почему Gen<Alphanumeric list> отображается как ввод в следующем map. Все по дизайну.

Операция, которую вы действительно хотите, обычно называется bind. Такая функция будет иметь сигнатуру (f: 'a -> Gen<'b>) -> Gen<'a> -> Gen<'b>. То есть потребуется функция, которая выдает другое Gen, а не чистое значение.

К сожалению, по какой-то причине Gen не раскрывает bind как таковой. Он доступен как часть gen построителя вычислительных выражений или как оператор >>= (который де-факто стандартный оператор для представления bind).

Учитывая приведенное выше объяснение, вы можете перефразировать свое определение следующим образом:

static member RelativeUrl_1() =
    Arb.generate<int> 
    |> Gen.suchThat (fun x -> x > 0 && x <= 10)
    >>= (fun length -> Gen.listOfLength length <| Generators.Alphanumeric())
    |> Gen.map (fun list -> String.Join("/", list))

Вы также можете рассмотреть возможность использования вычислительного выражения для создания генератора. К сожалению, для построителя выражений gen не определено where, поэтому вам все равно придется использовать suchThat для фильтрации. Но, к счастью, есть специальная функция Gen.choose для получения значения в заданном диапазоне:

static member RelativeUrl_1() =
  gen {
    // let! length = Arb.generate<int> |> Gen.suchThat (fun l -> l > 0 && l <= 10)
    let! length = Gen.choose (1, 10)
    let! list = Gen.listOfLength length <| Generators.Alphanumeric()
    return String.Join ("/", list)
  }
person Fyodor Soikin    schedule 31.03.2016
comment
Большое спасибо за отличное объяснение, Федор! Однако я не понял, что не так с Gen.choose, который Марк использовал в своем ответе. Вы сказали, что это не будет частью результирующего генератора, но мне кажется, что это так (см. код Марка с вычислительным выражением gen). - person Vagif Abilov; 01.04.2016
comment
Марк неправильно понял мой комментарий. Он предложил не просто использовать Gen.choose, а использовать его вместо Random. Если вы замените использование Random в своем первом блоке кода на Gen.choose, вам нужно будет сразу же попробовать его, что сделает его бесполезным. Но если перестроить генератор так, чтобы Gen.choose правильно привязывался, то он, конечно, не бесполезен. - person Fyodor Soikin; 01.04.2016
comment
Извините, я тоже неправильно понял ваш комментарий. Я думал, что вы вообще не поддерживаете Gen.choose, я и я нашли его весьма убедительным, поэтому я принял ответ Марка. Теперь я получил полную картину, и, конечно же, вы пришли первым с отличным предложением. - person Vagif Abilov; 01.04.2016

Комментарий Федора Сойкина предполагает, что Gen.choose бесполезен, поэтому, возможно, я что-то упускаю, но вот моя попытка:

open System
open FsCheck

let alphanumericChar = ['a'..'z'] @ ['A'..'Z'] @ ['0'..'9'] |> Gen.elements
let alphanumericString =
    alphanumericChar |> Gen.listOf |> Gen.map (List.toArray >> String)

let relativeUrl = gen {
    let! size = Gen.choose (1, 10)
    let! segments = Gen.listOfLength size alphanumericString
    return String.concat "/" segments }

Кажется, это работает:

> Gen.sample 10 10 relativeUrl;;
val it : string list =
  ["IC/5p///G/H/ur/vs//"; "l/mGe8spXh//au2WgdL/XvPJhey60X";
   "dxr/0y/1//P93/Ca/D/"; "R/SMJ3BvsM/Fzw4oifN71z"; "52A/63nVPM/TQoICz";
   "Co/1zTNKiCwt1/y6fwDc7U1m/CSN74CwQNl/olneBaJEB/RFqKiCa41l//ADo2MIUPFM/vG";
   "Zm"; "AxRpJ/fP/IOvpX/3yo"; "0/6QuDwiEgC/IpXRO8GA/E7UB8"; "jK/C/X/E4/AL3"]

Обратите внимание, что мое определение alphanumericString может генерировать пустые строки, поэтому иногда, как вы можете видеть из приведенного выше примера выходных данных FSI, оно будет генерировать относительные значения URL с пустыми сегментами.

Я оставлю читателю задание определить непустые буквенно-цифровые строки в качестве упражнения. Если вам нужна помощь с этим, пожалуйста, задайте другой вопрос и отправьте мне пинг ;)

person Mark Seemann    schedule 01.04.2016
comment
Большое спасибо! Кстати, ваш курс Pluralsight стал для меня поворотным моментом в том, чтобы наконец начать использовать FsCheck в своих проектах, и это привело меня к моему вопросу, поэтому кажется правильным, что вы также ответили на него :-) Ваш код выглядит именно так, как я хотел (я упустил из виду вычислительное выражение gen, которое необходимо в решении). Я озадачен, почему Федору не понравилось использование Gen.choose, мне нужно лучше понять его. - person Vagif Abilov; 01.04.2016
comment
А когда дело доходит до непустых строк, разве не просто заменить Gen.listOf на Gen.nonEmptyListOf? - person Vagif Abilov; 01.04.2016
comment
@VagifAbilov да, вы также можете использовать Gen.nonEmptyListOf, если вам просто нужны непустые строки. Обратите внимание, что длина может также увеличиться до числа, превышающего 10. - person Kurt Schelfthout; 01.04.2016
comment
Ах да, тогда размер может быть больше 10. - person Vagif Abilov; 01.04.2016
comment
@MarkSeeman: Меня совершенно сбивает с толку, как вы не заметили, что ваш код почти дословно соответствует тому, что я дал в своем ответе на 13 часов раньше, чем ваш. Браво! - person Fyodor Soikin; 01.04.2016
comment
@FyodorSoikin Я был не в сети, когда вместе обдумывал свое предложение. После того, как я опубликовал, я заметил, поэтому я проголосовал за ваш ответ. - person Mark Seemann; 01.04.2016