Продолжения Scala: много сдвигов в последовательности - PullRequest
6 голосов
/ 28 февраля 2012

Я пытался обернуть голову вокруг сложных проблем печати с продолжением скалы.Я читал все материалы, которые я могу найти на нем, включая справочные документы в пакете продолжений.Я думаю, что до некоторой степени понял это, и это имеет НЕКОТОРЫЙ смысл, когда вы думаете об этом.

Я думаю, что мое понимание этого (и некоторые мои вопросы) может быть лучше всего подытожено этой программой:

package com.whatever;
import scala.util.continuations._;

object methods {

  /* The method takes an Int as its parameter.  Theoretically, at some point in the future,
   * it will return a Float to the remainder of the continuation.  This example does it
   * immediately but doesn't have to (for example it could be calling a network service
   * to do the transformation)
   * 
   * Float @cpsParam[Unit,Float] means that whatever part of the reset{} that is captured
   * as a closure should receive a Float and needn't return anything (would it be meaningful
   * if Unit were something else?)
   * 
   * The reason I have to return 0.toFloat is so the compiler can properly type the
   * method.  That zero will never go anywhere.  Is this a sign I'm doing it wrong?
   */
  def method1(param:Int): Float @cpsParam[Unit,Float] = shift { cb:(Float=>Unit) =>
    cb(param.toFloat); 
    0.toFloat;
  }

  /* This method is basically identical but returns a String instead of a Float (Again,
   * theoretically this would be done by a network service and cb would be called at some
   * point in the future.
   */
  def method2(param:Int): String @cpsParam[Unit,String] = shift { cb:(String=>Unit) =>
    cb(param.toString);
    ""
  }
}

object Main {
  def main(args:Array[String]):Unit = {
    reset {
      val f = methods.method1(5);
      println(f);
    }
  }
}

Между прочим, преступно, что StackOverflow не выделяет scala! (я поправляюсь; на самом деле он довольно неплохо работает, но только не в режиме предварительного просмотра)

У меня следующие вопросы:

  1. Судя по комментариям в программе выше, чего не хватает в моем понимании CPS scala?Была ли когда-нибудь ситуация, когда вы НЕ хотели бы, чтобы Unit как B в @cpsParam[B,C]?
  2. Вышеуказанная программа компилируется и работает (она печатает "5.0").Но проблема, с которой я сталкиваюсь сейчас, вызывает у меня путаницу, когда я меняю блок reset, чтобы попытаться вызвать method2 после method1:

(Очевидно, вы не можетеиметь кодовый блок сразу после списка)

reset {
  val f = methods.method1(5);
  println(f);
  val s = methods.method2(42);
  println(s);
}

Когда я делаю это (что кажется довольно простым делом), я получаю следующую ошибку компилятора при перезагрузке (это scala 2.10 Milestone 2):

illegal answer type modification: scala.util.continuations.cpsParam[Unit,Float] andThen scala.util.continuations.cpsParam[Unit,String]

Я интерпретирую это как «ваша первая смена возвращает число с плавающей запятой, а вторая смена возвращает строку, и вы не можете этого сделать».Это точно?Означает ли это, что вы не можете использовать CPS для выполнения двух (или более) вещей последовательно, если они не имеют одинаковый тип возврата?Потому что это кажется серьезным ограничением.Я предполагаю, что я либо 1) пропускаю что-то, что позволяет вам это сделать, либо б) пропускает какую-то очевидную причину, по которой это невозможно с CPS.Но какой это?

Я начинаю чувствовать, что вам нужно быть студентом постдоктора, чтобы понять CPS scala.Но я, конечно, еще не совсем там.

1 Ответ

4 голосов
/ 29 февраля 2012

После того, как я задал этот вопрос, я провел гораздо больше исследований, и я думаю, что теперь я могу ответить на свой собственный вопрос (надеюсь, это не ошибочка).

Есть три вещи, которые яЭто помогло мне понять проблему, и я думаю, что любой, кто испытывает затруднения с продолжениями scala, преуспел бы в том, чтобы выполнить следующие шаги:

  1. Прочитайте оригинальную академическую статью о продолжениях scala .Это очень сухо, и я подозреваю, что это в основном бессмысленные бреды группы сумасшедших, но это также очень полезно, поскольку дает вам некоторое представление о том, как компилятор преобразует ваш код, управляемый продолжением, и о проблемах с типизацией и чистотой, которые онсталкивается с этим.
  2. Переписать свой код в стиле обратного вызова. Это единственная самая важная вещь, которую вы можете сделать, чтобы действительно понять, что происходит с потоком продолжений.и их типы.
  3. Изучите, и я имею в виду изучите сигнатуру типа shift и обратите внимание на то, что он делает.Это приведет вас к прозрению, которое у меня было.

В моем случае я набирал @cpsParam s и параметр cb в shift все неправильно.Я собираюсь объяснить, как я понял, что я делаю неправильно, так что любой, кто такой же глупый, как я, может выполнить те же шаги и, надеюсь, получить некоторое представление о том, когда компилятор продолжений сводит их с ума mad .

Шаг 1

Я прочитал вышеупомянутый документ.Около десятка раз.Я все еще очень мало понимаю об этом.Но то, что я понимаю, очень полезно.

Шаг 2

Я переписал свой reset блок в стиле обратного вызова, делая вид, что вместо shift каждый из методов имелвторой параметр с именем cb, который будет принимать функцию для выполнения оставшейся части блока.Вот как будет выглядеть блок сброса после этого:

  methods.method1(5, {f: Int => {
    println(f);
    methods.method2(42, {s: String => {
        println(s);
    });
  });

Видите, что происходит?Так что теперь вместо того, чтобы писать код, который кажется блокирующим, я сам разграничиваю продолжения и передаю их как функции каждому методу.Таким образом, для каждой из этих ситуаций становится ясно, что каждая из моих функций анонимного обратного вызова не должна ничего возвращать и, фактически, они оба должны возвращать Unit, или они будут загрязнять тип возврата метода, в который они передаются,Я думаю это то, что компилятор пытался сказать мне (хотя я могу ошибаться).

Вот как method1 должен выглядеть для моей программы в стиле обратного вызова

   def method1(param:Int, cb:(Float=>Unit)):Unit = {
     cb(param.toFloat);
   }

method2 аналогично, но занимает (String=>Unit).Теперь становится ясно, что мои методы также должны возвращать Unit, иначе они могут загрязнить возвращаемый тип функций обратного вызова.

Шаг 2 Заключение

Я думаю, что большая часть моей путаницы проистекает изДело в том, что по какой-то причине картина в моей голове заключалась в том, что каждый shift захватывался только до следующего shift как продолжение.Конечно, это не так;каждый shift должен захватывать весь остаток блока reset, включая все последующие shift s, чтобы он формировал большую вложенную ситуацию обратного вызова в обратном вызове.Кроме того, все обратные вызовы и все вызываемые CPS методы должны всегда (насколько я могу судить) возвращать Unit, потому что их результат не только никогда ничего не сделает, но и может загрязнить возвращаемый тип функции, которая их вызываети так далее по цепочке обратных вызовов.

Шаг 3

Теперь я посмотрел на подпись shift.Это было прямо передо мной:

def shift[A,B,C](fun: (A => B) => C)): A @cpsParam[B,C]

Когда я посмотрел на это, я понял, что в сочетании с моим упражнением в стиле обратного вызова, здесь было достаточно информации для меня (даже без полного понимания того, что shift делает за кадром), чтобы превратить это в в основном упражнение в размерном анализе.

Я знаю, что результат method1 будет Float.Поэтому обратный вызов продолжения (обозначенный (A => B) выше) должен принять Float в качестве параметра.Это исправляет A как Float.Поэтому method1 теперь выглядит следующим образом:

def method1(param:Int): Float @cpsParam[B,C] = shift { cb: (Float => B) => {
    ...
    C
  }
}

Другими словами, функция, которую я передаю shift, должна принимать функцию от Float до B и возвращать C. Хорошо, я знаю из своего упражнениячто обратный вызов должен вернуть модуль, или все станет грязно.Я также знаю, что в моем упражнении с обратным вызовом сами методы, очевидно, должны возвращать Unit, поскольку они передают свой фактический результат в качестве параметра для продолжения.Это аналогично тому, что C также является Unit.Таким образом, это означает, что method1 должно выглядеть следующим образом:

def method1(param:Int): Float @cpsParam[Unit,Unit] = shift { cb: (Float => Unit) => {
    cb(param);
  }
}

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

Что я выучил

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

Это означает, что, насколько я могу судить, для B и C не будет особой цели в shiftбыть чем-то иным, чем Unit.Это имеет смысл, потому что есть аннотация @suspendable, которая является сокращением для @cps[Unit], которая является сокращением для @cpsParam[Unit,Unit].

Я не знаю, почему примеры на scala-lang.orgтакая хреньНо на самом деле все, что им нужно было сказать: «если вам нужно использовать что-то кроме MyReturnType @suspendable, то вы, вероятно, делаете это неправильно, и, кстати, параметр функции, который принимает shift, должен также, вероятно, возвращать Unit».Тогда у меня все еще будут последние несколько драгоценных дней в моей жизни.

Счастливое окончание

Программа с изменениями, которые я отметил выше, полностью компилируется и запускается с обоими методами по очереди.Так что это заставляет меня верить, что я наконец-то все понял правильноЕсли вы доктор наук с глубоким пониманием CPS, пожалуйста, исправьте любые неточности в моих словах.

...