Одним из наиболее важных аспектов функционального программирования являются неизменяемые структуры данных, более известные как значения. Обновление этих структур с использованием подхода копирования при записи очень неэффективно, и именно поэтому были созданы постоянные структуры данных .
С другой стороны, JSON — это легкий текстовый формат обмена данными, не зависящий от языка. Он стал настолько популярным благодаря своей простоте . Существует множество библиотек для работы с JSON в экосистеме JVM; однако ни один из них не использует постоянные структуры данных .
В большинстве случаев эти библиотеки анализируют строку или массив байтов в объекте. Дело в том, зачем это делать? JSON — отличная структура. Это просто, легко агрегировать, легко создавать, легко рассуждать, так зачем создавать еще одну абстракцию над JSON?
Кроме того, есть много архитектур, которые работают с JSON сквозной. Переход от JSON к объектам или строкам назад и вперед не очень эффективен, особенно когда копирование при записи является единственной возможностью избежать мутации. Все эти моменты лучше проработаны в разговоре о ценности , шедевре от Rich Hickey . JSon-значениебиблиотека, которую я представляю здесь, была названа в честь этого разговора.
json-values — это функциональная библиотека JSON в Scala, которая использует постоянные структуры данных. В этой первой статье мы сосредоточимся на двух более важных аспектах разработки программного обеспечения, где значения json могут иметь значение: проверка данных и тестирование.
Чтобы проверить значения json, я провел тестирование на основе свойств с помощью ScalaCheck . Это отличная библиотека, где создавать генераторы и создавать их очень просто. Чтобы сгенерировать значения JSON, я создал json-values-generators , серию генераторов для работы с ScalaCheck. Я утверждаю, что это самый декларативный и красивый генератор Json в мире!
Вам также может понравиться:
Использование JSON с Play и Scala .
Валидация чрезвычайно важна в программном обеспечении. Поврежденные данные могут распространяться по всей вашей системе и вызывать кошмар. Ошибки, которые взрываются на вашем лице, намного лучше Если подумать, то определение, проверка и генерация значения JSON могут быть реализованы с использованием той же структуры данных; в конце концов, три из них — это просто привязки с различными элементами: значения, генераторы значений или спецификации значений. Давайте посмотрим на пример:
Scala
xxxxxxxxxx
1
import value.{JsObj,JsArray}
2
import value.Preamble._
3
4
// DEFINING A JSON
5
6
JsObj("name" -> "Rafael",
7
"age" -> 37,
8
"languages" -> JsArray("Haskell", "Scala", "Java", "Clojure")
9
"github" -> "imrafaelmerino",
10
"profession" -> "frustrated consultant",
11
"address" -> JsObj("city" -> "Madrid",
12
"location" -> JsArray(40.566, 87.987),
13
"country" -> "ES"
14
)
15
)
16
17
// DEFINING A JSON SPEC
18
19
import value.spec.{JsObjSpec,JsArraySpec}
20
import value.spec.JsNumberSpecs._
21
import value.spec.JsStrSpecs._
22
import value.spec.JsArraySpecs._
23
24
JsObjSpec("name" -> str,
25
"age" -> int,
26
"languages" -> arrayOfStr,
27
"github" -> str,
28
"profession" -> str,
29
"address" -> JsObjSpec("number" -> str,
30
"location" -> JsArraySpec(decimal, decimal),
31
"country" -> str
32
)
33
)
34
35
// DEFINING A JSON GENERATOR
36
37
import valuegen.{JsObjGen,JsArrayGen}
38
import valuegen.Preamble._
39
import org.scalacheck.Arbitrary.arbitrary
40
import org.scalacheck.Gen
41
42
JsObjGen("name" -> arbitrary[String],
43
"age" -> Gen.choose(18,120),
44
"profession" -> arbitrary[String],
45
"address" -> JsObjGen("city" -> arbitrary[String],
46
"location" -> JsArrayGen(doubleGen,doubleGen)
47
"country" -> Gen.oneOf("ES","PO")
48
)
49
)
Как видите, создание спецификаций и генераторов так же просто, как создание необработанного JSON. Написание спецификаций и генераторов для наших тестов — детская игра. У этого есть огромные преимущества для развития, такие как:
- Повысить продуктивность.
- Более читаемый код . Чем более читаемый код, тем легче поддерживать и рассуждать об этом коде.
Давайте сосредоточимся на том, что я называю json-spec. Я назвал его в честь спецификации , библиотеки Clojure. Это действительно мощный и составной. Любая спецификация может быть определена в терминах предикатов. Давайте приведем более сложный пример:
Джава
x
1
val ageSpec: Int => Result = age =>
2
if(age < 0) Invalid("you must be kidding!")
3
else if(age > 120) Invalid("you can stil fill out forms!!")
4
else Valid
5
6
val noLongerThat: Int => String => Result = max => value =>
7
if(value.length > max) Invalid("too long")
8
else Valid
9
10
val personSpec = JsObjSpec("name" -> str(noLongerThat(100)),
11
"last_name" -> str(noLongerThat(200)),
12
"profession" -> str(noLongerThat(200), nullable=true),
13
"country" -> enum("ES","PO"),
14
"languages" -> arrayOfStr(elemNullable=false),
15
"age" -> intSuchThat(ageSpec,required=false),
16
* -> any
17
)
18
// * -> any means: any binding different than the specified is allowed
19
20
// specs are easy to compose
21
22
val address = JsObjSpec("number" -> str,
23
"street" -> str,
24
"city" -> str,
25
"location" -> JsArraySpec(double, double)
26
)
27
28
val personWithAddressSpec = personSpec + ("address", address)
Спецификация может использоваться для проверки существующего JSON:
Scala
xxxxxxxxxx
1
import value.JsObjParser
2
import value.Invalid
3
import value.JsPath
4
5
val spec:JsObjSpec = ??? // a json spec
6
val str:String = ??? // a string representing a json
7
val json:Either[Invalid,JsObj] = JsObjParser.parse(str)
8
9
// a lazy list of path/error pairs is returned
10
val errors:LazyList[(JsPath,InvalidValue)] = json.fold(throw _)
11
(obj => obj.validate(spec))
Возвращение отложенного списка отделяет функцию проверки от ее потребителей. Потребители могут принять только ошибку, если им просто интересно, является ли JSON действительным или нет, или они могут принять их всех для распечатки отчета.
В обоих случаях они вызывают одну и ту же функцию. Спецификация json также может быть использована для анализа строки или массива байтов в JSON. Вместо того, чтобы анализировать всю строку и затем проверять ее, мы можем проверить JSON при ее анализе и остановить анализ, как только произойдет ошибка. В конце концов, важно также быстро потерпеть неудачу!
Scala
xxxxxxxxxx
1
val bytes:Array[Byte] = ??? // bytes representing a Json
2
val spec = ??? // a json spec
3
val parser = JsObjParser(spec) // a json parser from a spec
4
val result:Either[InvalidJson,JsObj] = parser.parse(bytes)
Давайте продолжим и представим некоторые из наиболее важных функций в библиотеке:
Scala
x
1
// every element is a JsValue. It's a sum type:
2
3
val value: JsValue = ???
4
5
value match
6
{
7
case primitive: JsPrimitive => primitive match
8
{
9
case JsStr(value) => println("I'm a string")
10
case number: JsNumber => number match
11
{
12
case JsInt(value) => println("I'm an integer")
13
case JsDouble(value) => println("I'm a double")
14
case JsLong(value) => println("I'm a long")
15
case JsBigDec(value) => println("I'm a big decimal")
16
case JsBigInt(value) => println
17
("I'm a big integer")
18
}
19
case JsBool(value) => println("I'm a boolean")
20
case JsNull => println("I'm null")
21
}
22
case json: Json[_] => json match
23
{
24
case o: JsObj => println("I'm an object")
25
case a: JsArray => println("I'm an array")
26
}
27
case JsNothing => println("I'm a special type!")
28
}
29
30
// A Json can be modeled as a set of path/value pairs:
31
32
val obj = JsObj("a" -> "hi",
33
"b" -> JsArray(1,2,3),
34
"c" -> JsObj("d" -> true,
35
"e" -> JsNull
36
)
37
)
38
39
val pairs:LazyList[(JsPath,JsValue)] = obj.flatten pairs.foreach(println)
40
41
// ("a", "hi")
42
// ("b" / 0, 1)
43
// ("b" / 1, 2)
44
// ("b" / 2, 3)
45
// ("c" / "d", true)
46
// ("c" / "e", null)
47
48
// in fact, a Json can be created from a set of path/value pairs:
49
50
val obj = JsObj(("a", "hi" ),
51
("b" / 0, 1 ),
52
("b" / 1, 2 ),
53
("b" / 2, 3 ),
54
("c" / "d", true ),
55
("c" / "e", null )
56
)
57
58
// lets get some value out using the apply function
59
60
obj("a") == JsStr("hi")
61
obj("b" / 0) == JsInt(1)
62
obj("c" / "d") == TRUE
63
obj("c" / "e") == JsNull
64
65
// apply is a total function, it always returns a JsValue:
66
67
obj("d" / "e") == JsNothing
68
69
// let's put some data in
70
71
val json = JsObj.empty.inserted("a" / "b", 1)
72
.inserted("a" / "c" / 2, 10, padWith=0)
73
74
json == JsObj("a" -> JsObj("b" -> 1,
75
"c" -> JsArray(0,0,10)
76
)
77
)
78
79
// trim string values. Imperative impl of fn. Later a Prism will be used
80
81
val trimStr: JsPrimitive => JsValue =
82
x => if (x.isStr) x.toJsStr.map(_.trim) else x
83
84
json map trimStr
85
86
// map keys to lowercase
87
88
val toLowerCase: String => String =_.toLowerCase
89
90
json mapKeys toLowerCase
91
92
// remove null values
93
94
val isNotNull: JsPrimitive => Boolean = _.isNotNull
95
96
json filter isNotNull
97
98
// remove empty keys
99
100
val isNotBlank: String => Boolean = !_.isBlank
101
102
json filterKeys isNotBlank
В map
функциях, перечисленных выше, важно отметить , что они являются функторами , поэтому структура JSON не меняется.
Оптика для работы со значениями json была определена в другой библиотеке . Я не хотел, чтобы обо мне говорили. Есть несколько библиотек для создания оптики, и пользователи json-values могут свободно использовать свои любимые. Я использую монокль . Например, Prisms действительно полезны для передачи функций для фильтрации и отображения :
Scala
xxxxxxxxxx
1
import JsStrOptics.toJsStr
2
// toJsStr :: Prism[JsValue,String]
3
4
val toLowerCase = toJsStr.modify(_.toLowerCase)
5
// JsValue => JsValue
6
7
val trim = toJsStr.modify(_.trim)
8
// JsValue => JsValue
9
10
val isNotEmpty = toJsStr.exist(_ != "")
11
// JsValue => JsValue
12
13
// prism and map/filter are good friends:
14
15
obj map toLowerCase
16
17
obj map trim
18
19
obj filter isNotEmpty
Это просто краткое введение в json-значения. Перейти на страницу проекта для получения дополнительной информации. В данный момент я перевожу библиотеку в Scala 3 и добавляю еще немного документации.
Подведение итогов:
- json-values реализует неизменный Json с постоянными структурами данных.
- Спецификации и генераторы являются составными структурами данных, которые определяются как Jsons.
- json-values прост, и, как сказал Рич Хикки, простота имеет значение .
- Scala — отличный язык, который позволяет разработчикам писать выразительный и красивый код.