В чем разница между =>, () => и Unit =>

153

Я пытаюсь представить функцию, которая не принимает аргументов и не возвращает значения (я имитирую функцию setTimeout в JavaScript, если вы должны знать.)

case class Scheduled(time : Int, callback :  => Unit)

не компилируется, говоря: "параметры val не могут быть названы по имени"

case class Scheduled(time : Int, callback :  () => Unit)  

компилируется, но должен вызываться странным образом, а не

Scheduled(40, { println("x") } )

Я должен сделать это

Scheduled(40, { () => println("x") } )      

Что также работает

class Scheduled(time : Int, callback :  Unit => Unit)

но вызывается еще менее разумным способом

 Scheduled(40, { x : Unit => println("x") } )

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

 Scheduled(40, println("x") )

Дай ребенку свою бутылочку!

Malvolio
источник
3
Другой способ использовать классы case с паролями по имени - это поместить их в список вторичных параметров, например case class Scheduled(time: Int)(callback: => Unit). Это работает, потому что список вторичных параметров не предоставляется публично и не включается в сгенерированные equals/ hashCodeметоды.
Nilskp
В этом вопросе и ответе есть еще несколько интересных аспектов, касающихся различий между параметрами по именам и функциями 0-арности . Это то, что я искал, когда нашел этот вопрос.
lex82

Ответы:

234

Call-by-Name: => Тип

=> TypeОбозначение означает-имя-вызов по, который является одним из многих способов , параметры могут быть переданы. Если вы с ними не знакомы, я рекомендую потратить некоторое время на чтение этой статьи в Википедии, хотя в настоящее время это в основном вызов по значению и вызов по ссылке.

Это означает, что то, что передается, заменяется именем значения внутри функции. Например, возьмите эту функцию:

def f(x: => Int) = x * x

Если я назову это так

var y = 0
f { y += 1; y }

Тогда код будет выполняться так

{ y += 1; y } * { y += 1; y }

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

Есть несколько других моментов, связанных с именами, о которых я расскажу после объяснения двух других.

Функции 0-арности: () => Тип

Синтаксис () => Typeобозначает тип Function0. То есть функция, которая не принимает параметров и что-то возвращает. Это эквивалентно, скажем, вызову метода size()- он не принимает параметров и возвращает число.

Интересно, однако, что этот синтаксис очень похож на синтаксис литерала анонимной функции , что является причиной некоторой путаницы. Например,

() => println("I'm an anonymous function")

является литералом анонимной функции arity 0, тип которого

() => Unit

Таким образом, мы могли бы написать:

val f: () => Unit = () => println("I'm an anonymous function")

Однако важно не путать тип со значением.

Unit => Type

Это на самом деле просто Function1, чей первый параметр имеет тип Unit. Другие способы написать это было бы (Unit) => Typeили Function1[Unit, Type]. Дело в том, что ... вряд ли это когда-нибудь будет тем, чего ты хочешь. В Unitосновной цели Type является указание на значение один не интересуют, поэтому не имеет смысла , чтобы получить это значение.

Рассмотрим, например,

def f(x: Unit) = ...

Что можно было сделать с x? Он может иметь только одно значение, поэтому его не нужно получать. Одним из возможных применений было бы возвращение функций цепочки Unit:

val f = (x: Unit) => println("I'm f")
val g = (x: Unit) => println("I'm g")
val h = f andThen g

Поскольку andThenон определен только Function1, а функции, которые мы объединяем, возвращаются Unit, мы должны были определить их как типы, Function1[Unit, Unit]чтобы иметь возможность их связывать .

Источники путаницы

Первым источником путаницы является то, что сходство между типом и литералом, которое существует для функций 0-арности, также существует для вызова по имени. Другими словами, думая, что, потому что

() => { println("Hi!") }

это буквально для () => Unit, то

{ println("Hi!") }

было бы буквально для => Unit. Это не. Это блок кода , а не литерал.

Другим источником путаницы является то, Unitчто записано значение типа (), которое выглядит как список параметров 0-арности (но это не так).

Даниэль С. Собрал
источник
Возможно, мне придется быть первым, кто проголосует против после двух лет. Кто-то интересуется синтаксисом case => на Рождество, и я не могу рекомендовать этот ответ как канонический и полный! К чему мир приближается? Может быть, майя были только на неделю. Правильно ли они фигурируют в високосные годы? Дневного сбережения?
Сом-Снитт
@ som-snytt Ну, вопрос не задавался case ... =>, поэтому я не упомянул об этом. Грустно, но правда. :-)
Даниэль С. Собрал
1
@ Даниэль С. Собрал, не могли бы вы объяснить, «Это блок кода, а не литерал». часть. Так в чем же разница между двумя?
nish1013
2
@ nish1013 «Литерал» - это значение (целое число 1, символ 'a', строка "abc"или функция () => println("here"), в некоторых примерах). Его можно передавать в качестве аргумента, хранить в переменных и т. Д. «Блок кода» - это синтаксическое разграничение операторов - это не значение, его нельзя передать или что-то в этом роде.
Даниэль С. Собрал,
1
@ Алекс Это та же разница, что и (Unit) => Typeпротив () => Type- первый - это Function1[Unit, Type], а второй - Function0[Type].
Даниэль С. Собрал
36
case class Scheduled(time : Int, callback :  => Unit)

caseМодификатор делает неявное valиз каждого аргумента конструктора. Следовательно (как кто-то заметил), если вы удалите, caseвы можете использовать параметр call-by-name. Компилятор, возможно, в любом случае мог бы разрешить это, но он мог бы удивить людей, если бы он создал, val callbackа не превратился в lazy val callback.

Когда вы переходите на callback: () => Unitсейчас, ваш случай просто принимает функцию, а не параметр call-by-name. Очевидно, что функция может быть сохранена, val callbackтак что нет проблем.

Самый простой способ получить то, что вы хотите ( Scheduled(40, println("x") )где параметр call-by-name используется для передачи лямбды), это, вероятно, пропустить caseи явно создать то, applyчто вы не могли получить в первую очередь:

class Scheduled(val time: Int, val callback: () => Unit) {
    def doit = callback()
}

object Scheduled {
    def apply(time: Int, callback: => Unit) =
        new Scheduled(time, { () => callback })
}

В использовании:

scala> Scheduled(1234, println("x"))
res0: Scheduled = Scheduled@5eb10190

scala> Scheduled(1234, println("x")).doit
x
Бен Джексон
источник
3
Почему бы не сохранить это case-class и просто переопределить применение по умолчанию? Кроме того, компилятор не может перевести имя по имени в lazy val, так как они имеют присущую ему различную семантику, lazy - не более одного раза, а имя - по каждой ссылке
Виктор Кланг,
@ViktorKlang Как вы можете переопределить метод применения класса case по умолчанию? stackoverflow.com/questions/2660975/…
Сойер
object ClassName {def apply (…):… =…}
Виктор Кланг,
Четыре года спустя, и я понимаю, что ответ, который я выбрал, ответил только на вопрос в названии, а не на тот, который у меня был на самом деле (на который этот ответ действительно отвечает).
Мальволио
1

В вопросе вы хотите смоделировать функцию SetTimeOut в JavaScript. Основываясь на предыдущих ответах, я пишу следующий код:

class Scheduled(time: Int, cb: => Unit) {
  private def runCb = cb
}

object Scheduled {
  def apply(time: Int, cb: => Unit) = {
    val instance = new Scheduled(time, cb)
    Thread.sleep(time*1000)
    instance.runCb
  }
}

В REPL мы можем получить что-то вроде этого:

scala> Scheduled(10, println("a")); Scheduled(1, println("b"))
a
b

Наше моделирование не ведет себя точно так же, как SetTimeOut, потому что наше моделирование является функцией блокировки, но SetTimeOut не является блокирующим.

Джефф Сюй
источник
0

Я делаю это так (просто не хочу нарушать правила):

case class Thing[A](..., lazy: () => A) {}
object Thing {
  def of[A](..., a: => A): Thing[A] = Thing(..., () => a)
}

и назовите это

Thing.of(..., your_value)
Алексей Рыхальский
источник