В этой статье в мини-серии о Scalaz мы рассмотрим несколько дополнительных монад и паттернов, доступных в Scalaz. Еще раз, мы посмотрим на вещи, которые практично использовать, и избегать внутренних деталей или Скалаза. Чтобы быть более точным, в этой статье мы рассмотрим:
- Монада Writer: отслеживание своего рода регистрации во время набора операций
- Монада состояний: простой способ отслеживания состояния по ряду вычислений
- Объективы: легкий доступ к глубоко вложенным атрибутам и удобство копирования классов дел
В этой серии в настоящее время доступны следующие статьи:
- Функции Scalaz для повседневного использования, часть 1: классы типов и расширения Scala
- Возможности Scalaz для повседневного использования, часть 2: Monad Transformers и Reader Monad
- Возможности Scalaz для повседневного использования, часть 3: State Monad, Writer Monad и линзы
Начнем с одной из дополнительных монад, предоставляемых Скалазом.
Писатель монада
В основном каждый писатель имеет журнал и возвращаемое значение. Таким образом, вы можете просто написать свой чистый код и на более позднем этапе определить, что вы хотите сделать с ведением журнала (например, проверить его в тесте, вывести на консоль или в какой-нибудь файл журнала). Таким образом, мы могли бы использовать писателя, например, для отслеживания операций, которые мы выполнили, чтобы получить какое-то конкретное значение.
Итак, давайте посмотрим на код и посмотрим, как эта штука работает:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
|
import scalaz._ import Scalaz._ object WriterSample extends App { // the left side can be any monoid. E.g something which support // concatenation and has an empty function: e.g. String, List, Set etc. type Result[T] = Writer[List[String], T] def doSomeAction() : Result[Int] = { // do the calculation to get a specific result val res = 10 // create a writer by using set res.set(List(s "Doing some action and returning res" )) } def doingAnotherAction(b: Int) : Result[Int] = { // do the calculation to get a specific result val res = b * 2 // create a writer by using set res.set(List(s "Doing another action and multiplying $b with 2" )) } def andTheFinalAction(b: Int) : Result[String] = { val res = s "bb:$b:bb" // create a writer by using set res.set(List(s "Final action is setting $b to a string" )) } // returns a tuple (List, Int) println(doSomeAction().run) val combined = for { a <- doSomeAction() b <- doingAnotherAction(a) c <- andTheFinalAction(b) } yield c // Returns a tuple: (List, String) println(combined.run) } |
В этом примере у нас есть три операции, которые что-то делают. В этом случае, они на самом деле не так много, но это не имеет значения. Главное, чтобы вместо возврата значения мы возвращали Writer (обратите внимание, что мы могли бы также создать Writer для понимания), используя функцию set . Когда мы вызываем run на Writer, мы получаем не только результат операции, но и агрегированные значения, собранные Writer . Итак, когда мы делаем:
01
02
03
04
05
06
07
08
09
10
|
type Result[T] = Writer[List[String], T] def doSomeAction() : Result[Int] = { // do the calculation to get a specific result val res = 10 // create a writer by using set res.set(List(s "Doing some action and returning res" )) } println(doSomeAction().run) |
Результат выглядит следующим образом: (Список (Выполнение некоторых действий и возврат res), 10) . Это не так захватывающе, но становится интереснее, когда мы начинаем использовать авторов для понимания.
1
2
3
4
5
6
7
8
|
val combined = for { a <- doSomeAction() b <- doingAnotherAction(a) c <- andTheFinalAction(b) } yield c // Returns a tuple: (List, String) println(combined.run) |
Когда вы посмотрите на результат этого, вы увидите что-то вроде:
1
2
3
4
|
(List(Doing some action and returning res, Doing another action and multiplying 10 with 2 , Final action is setting 20 to a string) ,bb: 20 :bb) |
Как вы можете видеть, мы собрали все различные сообщения журнала в List [String], и результирующий кортеж также содержит окончательное вычисленное значение.
Если вы не хотите добавлять экземпляр Writer в свои функции, вы также можете просто создать писателей для понимания:
1
2
3
4
5
6
7
8
|
val combined2 = for { a <- doSomeAction1() set( " Executing Action 1 " ) // A String is a monoid too b <- doSomeAction2(a) set( " Executing Action 2 " ) c <- doSomeAction2(b) set( " Executing Action 3 " ) // c <- WriterT.writer("bla", doSomeAction2(b)) // alternative construction } yield c println(combined2.run) |
Результат этого примера:
1
|
( Executing Action 1 Executing Action 2 Executing Action 3 , 5 ) |
Круто верно? Для этого примера мы показали только основные вещи Writer, где тип — это просто простой тип. Конечно, вы также можете создавать экземпляры Writer из более сложных типов. Примеры этого можно найти здесь: http://stackoverflow.com/questions/35362240/creating-a-writertf-wa-from-a-writerw-a
Государственная монада
Еще одна интересная монада, это государственная монада. Монада состояний обеспечивает удобный способ обработки состояний, которые необходимо передать через набор функций. Вам может потребоваться отслеживать результаты, передавать некоторый контекст вокруг набора функций или требовать некоторый (не изменяемый) контекст по другой причине. С ( монадой Reader ) мы уже видели, как вы можете внедрить некоторый контекст в функцию. Этот контекст, однако, не был изменчив. С помощью монады состояний мы получили хороший шаблон, который мы можем использовать для передачи изменяемого контекста безопасным и чистым способом.
Давайте посмотрим на некоторые примеры:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
|
case class LeftOver(size: Int) /** A state transition, representing a function `S => (S, A)`. */ type Result[A] = State[LeftOver, A] def getFromState(a: Int): Result[Int] = { // do all kinds of computations State[LeftOver, Int] { // just return the amount of stuff we got from the state // and return the new state case x => (LeftOver(x.size - a), a) } } def addToState(a: Int): Result[Int] = { // do all kinds of computations State[LeftOver, Int] { // just return the amount of stuff we added to the state // and return the new state case x => (LeftOver(x.size + a), a) } } val res: Result[Int] = for { _ <- addToState( 20 ) _ <- getFromState( 5 ) _ <- getFromState( 5 ) a <- getFromState( 5 ) currentState <- get[LeftOver] // get the state at this moment manualState <- put[LeftOver](LeftOver( 9000 )) // set the state to some new value b <- getFromState( 10 ) // and continue with the new state } yield { println(s "currenState: $currentState" ) a } // we start with state 10, and after processing we're left with 5 // without having to pass state around using implicits or something else println(res(LeftOver( 10 ))) |
Как видите, в каждой функции мы получаем текущий контекст, вносим в него некоторые изменения и возвращаем кортеж, состоящий из нового состояния и значения функции. Таким образом, каждая функция имеет доступ к состоянию, может возвращать новое и возвращает это новое состояние вместе со значением функции в виде кортежа. Когда мы запускаем приведенный выше код, мы видим следующее:
1
2
|
currenState: LeftOver( 15 ) (LeftOver( 8990 ), 5 ) |
Как видите, каждая из функций что-то делает с состоянием. С помощью функции get [S] мы можем получить значение состояния в текущий момент, и в этом примере мы распечатаем его. Помимо использования функции get , мы также можем установить состояние напрямую, используя функцию put .
Как вы можете видеть, это очень красивый и простой в использовании шаблон, но отлично подходит, когда вам нужно передать какое-то состояние вокруг набора функций.
линзы
Так что хватит пока с монадами, давайте посмотрим на линзы. С помощью линз можно легко (что гораздо проще, чем просто копировать классы дел вручную) изменять значения в иерархиях вложенных объектов. Линзы могут делать много всего, но в этой статье я расскажу лишь о некоторых основных функциях. Сначала код:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
import scalaz._ import Scalaz._ object LensesSample extends App { // crappy case model, lack of creativity case class Account(userName: String, person: Person) case class Person(firstName: String, lastName: String, address: List[Address], gender: Gender) case class Gender(gender: String) case class Address(street: String, number: Int, postalCode: PostalCode) case class PostalCode(numberPart: Int, textPart: String) val acc1 = Account( "user123" , Person( "Jos" , "Dirksen" , List(Address( "Street" , 1 , PostalCode( 12 , "ABC" )), Address( "Another" , 2 , PostalCode( 21 , "CDE" ))), Gender( "male" ))) val acc2 = Account( "user345" , Person( "Brigitte" , "Rampelt" , List(Address( "Blaat" , 31 , PostalCode( 67 , "DEF" )), Address( "Foo" , 12 , PostalCode( 45 , "GHI" ))), Gender( "female" ))) // when you now want to change something, say change the gender (just because we can) we need to start copying stuff val acc1Copy = acc1.copy( person = acc1.person.copy( gender = Gender( "something" ) ) ) |
В этом примере мы определили пару классов case и хотим изменить одно значение. Для case-классов это означает, что мы должны начать вложение набора операций копирования, чтобы правильно изменить одно из вложенных значений. Хотя это можно сделать для простых иерархий, это быстро становится громоздким. С lensen вам предложили механизм, позволяющий сделать это составным способом:
01
02
03
04
05
06
07
08
09
10
11
|
val genderLens = Lens.lensu[Account, Gender]( (account, gender) => account.copy(person = account.person.copy(gender = gender)), (account) => account.person.gender ) // and with a lens we can now directly get the gender val updated = genderLens.set(acc1, Gender( "Blaat" )) println(updated) #Output: Account(user123,Person(Jos,Dirksen,List(Address(Street, 1 ,PostalCode( 12 ,ABC)), Address(Another, 2 ,PostalCode( 21 ,CDE))),Gender(Blaat))) |
Таким образом, мы определяем объектив, который может изменить конкретное значение в иерархии. С помощью этого объектива мы теперь можем напрямую получить или установить значение во вложенной иерархии. Мы также можем создать линзу, которая изменяет значение и возвращает измененный объект за один раз, используя оператор => = .
1
2
3
4
5
6
7
8
9
|
// we can use our base lens to create a modify lens val toBlaBlaLens = genderLens =>= (_ => Gender( "blabla" )) println(toBlaBlaLens(acc1)) # Output: Account(user123,Person(Jos,Dirksen,List(Address(Street, 1 ,PostalCode( 12 ,ABC)), Address(Another, 2 ,PostalCode( 21 ,CDE))),Gender(blabla))) val existingGender = genderLens.get(acc1) println(existingGender) # Output: Gender(male) |
И мы можем использовать операторы > => и <= < для объединения линз. Например, в следующем примере кода мы создаем отдельные линзы, которые затем комбинируются и выполняются:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
|
// First create a lens that returns a person val personLens = Lens.lensu[Account, Person]( (account, person) => account.copy(person = person), (account) => account.person ) // get the person lastname val lastNameLens = Lens.lensu[Person, String]( (person, lastName) => person.copy(lastName = lastName), (person) => person.lastName ) // Get the person, then get the lastname, and then set the lastname to // new lastname val combined = (personLens >=> lastNameLens) =>= (_ => "New LastName" ) println(combined(acc1)) # Output: Account(user123,Person(Jos,New LastName,List(Address(Street, 1 ,PostalCode( 12 ,ABC)), Address(Another, 2 ,PostalCode( 21 ,CDE))),Gender(male))) |
Выводы
Есть еще две темы, о которых я хочу написать, это валидации и бесплатные монады. В следующей статье этой серии я покажу, как вы можете использовать ValidationNEL для валидации. Тем не менее, бесплатные монады, я думаю, на самом деле не относятся к категории повседневного использования, поэтому я потрачу еще пару статей на эту тему в будущем.
Ссылка: | Функции Scalaz для повседневного использования, часть 3: State Monad, Writer Monad и линзы от нашего партнера JCG Йоса Дирксена в блоге Smart Java . |