Подтвердить что ты не робот

Почему Scala не имеет функции возврата/единицы, определенной для каждой монады (в отличие от Haskell)?

В чем причина дизайнерского решения в Scala, что монады не имеют функции return/unit в отличие от Haskell, где каждая монада имеет функцию возврата, которая помещает значение в стандартный монадический контекст для данной монады?

Например, почему List, Option, Set и т.д. не имеют функций возврата/единицы, определенных в стандартной библиотеке, как показано на слайдах ниже?

Я спрашиваю об этом, потому что в реактивном курсе Курсера Мартин Одерски явно упомянул об этом, как видно ниже в слайдах, но не объяснил, почему Scala не имеет их, даже если unit/return является существенным свойством монады.

enter image description hereenter image description hereenter image description here

4b9b3361

Ответ 1

Как сказал Ørjan Johansen, Scala не поддерживает отправку метода по возвращаемому типу. Объектная система Scala построена по JVM one, а инструкция JVM invokevirtual, которая является основным инструментом динамического полиморфизма, отправляет вызов на основе типа объекта this.

В качестве дополнительной заметки диспетчеризация - это процесс выбора конкретного метода для вызова. В Scala/Java все методы являются виртуальными, то есть метод, который вызывается, зависит от фактического типа объекта.

class A { def hello() = println("hello method in A") }

class B extends A { override def hello() = println("hello method in B") }

val x: A = new A
x.hello()  // prints "hello method in A"

val y: A = new B
y.hello()  // prints "hello method in B"

Здесь, даже если переменная y имеет тип A, вызывается метод hello из B, потому что JVM "видит", что фактический тип объекта в y равен B и вызывает соответствующий метод.

Однако JVM принимает тип переменной, на которую метод учитывается. Например, невозможно вызвать разные методы, основанные на типе аргументов runtime без явных проверок. Например:

class A {
  def hello(x: Number) = println(s"Number: $x")
  def hello(y: Int) = println(s"Integer: $y")
}

val a = new A
val n: Number = 10: Int
a.hello(n)  // prints "Number: 10"

Здесь мы имеем два метода с тем же именем, но с разными типами параметров. И даже если n действительный тип Int, вызывается hello(Number) версия - он статически статично основан на статическом типе n (эта функция, статическое разрешение на основе типов аргументов, называется перегрузкой). Следовательно, динамическая отправка на аргументы метода отсутствует. Некоторые языки также поддерживают отправку аргументов метода, например, общие методы Lisp CLOS или Clojure работают так.

Haskell имеет усовершенствованную систему типов (она сопоставима с Scala, и на самом деле они оба происходят из System F, но система типа Scala поддерживает подтипирование, которое делает тип вывода намного сложнее), что позволяет глобальный тип вывода, по крайней мере, без определенных расширений. Haskell также имеет понятие классов типов, которое является его инструментом для динамического полиморфизма. Типовые классы можно легко воспринимать как интерфейсы без наследования, но с отправкой на типы параметров и возвращаемых значений. Например, это допустимый класс типа:

class Read a where
    read :: String -> a

instance Read Integer where
    read s = -- parse a string into an integer

instance Read Double where
    read s = -- parse a string into a double

Затем, в зависимости от контекста, где вызывается метод, можно вызвать функцию read для Integer или Double:

x :: Integer
x = read "12345"  // read for Integer is called

y :: Double
y = read "12345.0"  // read for Double is called

Это очень мощный метод, который не имеет соответствия в открытой объектной системе JVM, поэтому объектная система Scala тоже не поддерживает. Кроме того, отсутствие полномасштабного вывода типа сделало бы эту функцию несколько громоздкой в ​​использовании. Таким образом, стандартная библиотека Scala не имеет метода return/unit в любом месте - невозможно выразить ее с помощью обычной объектной системы, просто нет места, где такой метод может быть определен. Следовательно, концепция монады в Scala является неявной и условной - все с соответствующим методом flatMap можно считать монадой, и все с правильными методами можно использовать в конструкции for. Это очень похоже на утиную печать.

Однако система типа Scala вместе со своим механизмом implicits достаточно мощна, чтобы выражать полнофункциональные классы типов и, как правило, общие монады формальным образом, хотя из-за трудностей с полным типом вывода может потребоваться добавить больше типа, чем в Haskell.

Это определение класса типа monad в Scala:

trait Monad[M[_]] {
  def unit[A](a: A): M[A]
  def bind[A, B](ma: M[A])(f: A => M[B]): M[B]
}

И это его реализация для Option:

implicit object OptionMonad extends Monad[Option] {
  def unit[A](a: A) = Some(a)
  def bind[A, B](ma: Option[A])(f: A => Option[B]): Option[B] =
    ma.flatMap(f)
}

Затем это можно использовать общим способом следующим образом:

// note M[_]: Monad context bound
// this is a port of Haskell filterM found here:
// http://hackage.haskell.org/package/base-4.7.0.1/docs/src/Control-Monad.html#filterM
def filterM[M[_]: Monad, A](as: Seq[A])(f: A => M[Boolean]): M[Seq[A]] = {
  val m = implicitly[Monad[M]]
  as match {
    case x +: xs =>
      m.bind(f(x)) { flg =>
        m.bind(filterM(xs)(f)) { ys =>
          m.unit(if (flg) x +: ys else ys)
        }
      }
    case _ => m.unit(Seq.empty[A])
  }
}

// using it

def run(s: Seq[Int]) = {
  import whatever.OptionMonad  // bring type class instance into scope

  // leave all even numbers in the list, but fail if the list contains 13
  filterM[Option, Int](s) { a =>
    if (a == 13) None
    else if (a % 2 == 0) Some(true)
    else Some(false)
  }
}

run(1 to 16)  // returns None
run(16 to 32)  // returns Some(List(16, 18, 20, 22, 24, 26, 28, 30, 32))

Здесь filterM записывается в общем случае для любого экземпляра класса типа Monad. Поскольку неявный объект OptionMonad присутствует на сайте вызова filterM, он будет передан в filterM неявно, и он сможет использовать его методы.

Вы можете видеть сверху, что классы классов позволяют эмулировать отправку по типу возврата даже в Scala. Фактически, это именно то, что Haskell делает под обложками - и Scala, и Haskell пропускают словарь методов, реализующих некоторый тип класса, хотя в Scala он несколько более явный, потому что эти словари являются первоклассными объектами там и могут быть импортированы по требованию или даже переданы явно, поэтому на самом деле это не правильная диспетчеризация, поскольку она не встроена.

Если вам нужна эта сумма, вы можете использовать Scalaz библиотеку, которая содержит много типов классов (включая монаду) и их экземпляры для некоторых распространенных типов, включая Option.

Ответ 2

Я не думаю, что вы действительно говорите, что монады Scala не имеют единичной функции - скорее всего, имя функции единицы может отличаться. Это то, что, как представляется, показано во вторых примерах слайдов.

Что касается того, почему это так, я думаю, что это просто потому, что Scala работает на JVM, и эта функция должна быть реализована как методы JVM, которые однозначно идентифицируются с помощью:

  • класс, к которому они принадлежат;
  • их имя;
  • их типы параметров. Но они не идентифицируются по типу возврата. Поскольку тип параметра обычно не будет различать различные функции блока (обычно это просто общий тип), для них вам нужны разные имена.

На практике они часто реализуются как метод apply(x) на сопутствующем объекте класса монады. Например, для класса List единичная функция - это метод apply(x) на объекте List. По соглашению, List.apply(x) можно также назвать как List(x), что является более распространенным/идиоматическим.

Итак, я предполагаю, что Scala имеет хотя бы соглашение об именах для функции unit, хотя для него нет уникального имени:

// Some monad :
class M[T] {
  def flatMap[U](f: T => M[U]): M[U] = ???
}
// Companion object :
object M {
  def apply(x: T): M[T] = ??? // Unit function
}

// Usage of the unit function :
val x = ???
val m = M(x)

Ответ 3

Предостережение: Я все еще участвую в Haskell, и я как бы придумываю этот ответ, когда я иду.


Во-первых, то, что вы уже знаете, - что Haskell do desangars для привязки:

Заимствование этого примера из Википедии:

add mx my = do
  x <- mx
  y <- my
  return (x + y)

add mx my =
  mx >>= (\x ->
    my >>= (\y ->
      return (x + y)))

Scala аналог do является выражением for-yield. Он аналогичным образом уменьшает каждый шаг до flatMap (его эквивалент привязки).

Однако есть разница: последний <- в дескрипторах для-yield для map, а не flatMap.

def add(mx: Option[Int], my: Option[Int]) =
  for {
    x <- mx
    y <- my
  } yield x + y

def add(mx: Option[Int], my: Option[Int]) =
  mx.flatMap(x =>
    my.map(y =>
      x + y))

Итак, поскольку у вас нет "сглаживания" на последнем шаге, значение выражения уже имеет тип монады, поэтому нет необходимости "повторно обертывать" его чем-то, сравнимым с return.

Ответ 4

На самом деле есть функция возврата в scala. Это просто трудно найти.

Scala во многом отличается от Haskell. Большинство из этих различий являются прямыми последствиями ограничений JVM. JVM не может отправлять методы исходя из своего типа возврата. Таким образом, Scala представил полиморфизм типа класса, основанный на неявных доказательствах для устранения этого неудобства.

Он даже используется в стандартных коллекциях Scala. Вы можете заметить многочисленные использования CanBuildFrom и CanBuild implicits, используемых в коллекции Scala api. Например, scala.collection.immutable.List.

Каждый раз, когда вы хотите создать пользовательскую коллекцию, вы должны написать реализацию для этого implicits. Однако не так много руководств для написания. Я рекомендую вам данное руководство. Он показывает, почему CanBuildFrom так важен для коллекций и как он используется. На самом деле это просто еще одна форма функции return, и любой, кто знаком с монадами Хаскелла, ясно поймет ее важность.

Таким образом, вы можете использовать пользовательскую коллекцию в качестве примера монадов и писать другие монады на основе предоставленного учебника.