Скалаз: запрос на использование сценария Коклейсли - PullRequest
17 голосов
/ 01 апреля 2010

Этот вопрос не является пламенной приманкой! Как это может быть очевидно, я недавно смотрел на Scalaz . Я пытаюсь понять , почему мне нужны некоторые функции, которые предоставляет библиотека. Вот что-то:

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. Две вещи происходят со мной:

  1. Как я мог знать, что мой NEL будет уменьшен таким образом из сигнатур методов? (Я вообще не ожидал результата )
  2. Какой смысл в этом? Можно ли извлечь из меня достаточно простой и понятный пример использования?

Этот вопрос - тонко завуалированная просьба к какому-то прекрасному человеку, например retronym , объяснить, как на самом деле работает эта мощная библиотека.

Ответы [ 2 ]

18 голосов
/ 01 апреля 2010

Чтобы понять результат, вам нужно понять экземпляр 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
9 голосов
/ 02 апреля 2010

Cojoin также определено для scalaz.Tree и 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 берет дерево и возвращает его копию, где каждый узел имеет аннотацию с максимальным расстоянием от листа.

...