Скалаз:запрос варианта использования для состава Коклейсли
-
23-09-2019 - |
Вопрос
Этот вопрос не задуман как провокация!Как это может быть очевидно, я рассматривал Скалаз недавно.Я пытаюсь понять почему Мне нужны некоторые функциональные возможности, которые предоставляет библиотека.Вот кое-что:
import scalaz._
import Scalaz._
type NEL[A] = NonEmptyList[A]
val NEL = NonEmptyList
Я поместил несколько инструкций println в свои функции, чтобы посмотреть, что происходит (в сторону:что бы я сделал, если бы пытался избежать подобных побочных эффектов?).Мои функции заключаются в следующем:
val f: NEL[Int] => String = (l: NEL[Int]) => {println("f: " + l); l.toString |+| "X" }
val g: NEL[String] => BigInt = (l: NEL[String]) => {println("g: " + l); BigInt(l.map(_.length).sum) }
Затем я объединяю их с помощью коклейсли и пройти в NEL[Int]
val k = cokleisli(f) =>= cokleisli(g)
println("RES: " + k( NEL(1, 2, 3) ))
Что это печатает?
f: NonEmptyList(1, 2, 3)
f: NonEmptyList(2, 3)
f: NonEmptyList(3)
g: NonEmptyList(NonEmptyList(1, 2, 3)X, NonEmptyList(2, 3)X, NonEmptyList(3)X)
RES: 57
Значение RES - это количество символов элементов (String) в конечном NEL.Мне приходят в голову две вещи:
- Как я мог знать, что мой NEL будет уменьшен таким образом, исходя из сигнатур используемых методов?(Я не ожидал такого результата вообще)
- В чем смысл всего этого?Можно ли разработать для меня достаточно простой и понятный пример использования?
Этот вопрос - тонко завуалированная мольба к такому милому человеку, как ретроним чтобы объяснить, как на самом деле работает эта мощная библиотека.
Решение
Чтобы понять результат, вам нужно понять Comonad[NonEmptyList]
пример. Comonad[W]
по сути, предоставляет три функции (фактический интерфейс в Scalaz немного отличается, но это помогает с объяснением):
map: (A => B) => W[A] => W[B]
copure: W[A] => A
cojoin: W[A] => W[W[A]]
Итак, Comonad
предоставляет интерфейс для некоторого контейнера W
который имеет выделяющийся элемент "head" (copure
) и способ показать внутреннюю структуру контейнера, чтобы мы получили по одному контейнеру на элемент (cojoin
), каждый с заданным элементом во главе.
Способ, которым это реализовано для NonEmptyList
это что copure
возвращает начало списка и cojoin
возвращает список списков, с этим списком в начале и всеми концами этого списка в конце.
Пример (я сокращаю NonEmptyList
Для Nel
):
Nel(1,2,3).copure = 1
Nel(1,2,3).cojoin = Nel(Nel(1,2,3),Nel(2,3),Nel(3))
Тот самый =>=
функция заключается в составлении коКлеизли.Как бы вы составили две функции f: W[A] => B
и g: W[B] => C
, ничего не зная о них , кроме этого W
является Comonad
?Тип входного сигнала f
и тип выходного сигнала g
они несовместимы.Тем не менее, вы можете map(f)
чтобы получить W[W[A]] => W[B]
а затем составьте это с помощью g
.Теперь, учитывая W[A]
, вы можете cojoin
это для того, чтобы получить W[W[A]]
чтобы использовать эту функцию.Итак, единственная разумная композиция - это функция k
это приводит к следующему:
k(x) = g(x.cojoin.map(f))
Итак, для вашего непустого списка:
g(Nel(1,2,3).cojoin.map(f))
= g(Nel(Nel(1,2,3),Nel(2,3),Nel(3)).map(f))
= g(Nel("Nel(1,2,3)X","Nel(2,3)X","Nel(3)X"))
= BigInt(Nel("Nel(1,2,3)X","Nel(2,3)X","Nel(3)X").map(_.length).sum)
= BigInt(Nel(11,9,7).sum)
= 27
Другие советы
Cojoin также определен для скалаз.Дерево и scalaz.TreeLoc.Это может быть эксплуатируемый найти поток всех путей от корня дерева до каждого листового узла.
def leafPaths[T](tree: Tree[T]): Stream[Stream[T]]
= tree.loc.cojoin.toTree.flatten.filter(_.isLeaf).map(_.path)
Используя композицию стрелки coKleisli, мы можем сделать это, например:
def leafDist[A] = (cokleisli(leafPaths[A]) &&& cokleisli(_.rootLabel))
=>= (_.map(s => (s._2, s._1.map(_.length).max)))
leafDist
берет дерево и возвращает его копию, в которой каждый узел отмечен максимальным расстоянием от листа.