Реализация ifTrue, ifFalse, ifSome, ifNone и т. Д. В Scala, чтобы избежать if (...) и простого сопоставления с образцом - PullRequest
7 голосов
/ 13 апреля 2011

В Scala я постепенно утратил привычку думать на Java / C, ориентируясь на потоки управления, и привык сначала идти вперед и получать интересующий меня объект, а затем обычно применяю что-то вроде match или map() или foreach() для коллекций.Мне это очень нравится, так как теперь это выглядит как более естественный и более точный способ структурирования моего кода.

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

Сравнение:

obj.isSomethingValid match {
  case true => doX
  case false => doY
}

против.то, что я написал бы со стилем, близким к Java:

if (obj.isSomethingValid)
  doX
else
  doY

Тогда я вспомнил сообщения ifTrue: и ifFalse: Smalltalk (и их варианты).Можно ли написать что-то подобное в Scala?

obj.isSomethingValid ifTrue doX else doY

с вариантами:

val v = obj.isSomethingValid ifTrue someVal else someOtherVal

// with side effects
obj.isSomethingValid ifFalse {
  numInvalid += 1
  println("not valid")
}

Кроме того, можно ли сделать этот стиль доступным для простых типов с двумя состояниями, таких как Option?Я знаю, что более идиоматический способ использования Option состоит в том, чтобы рассматривать его как коллекцию и вызывать filter(), map(), exists(), но часто, в конце, я обнаруживаю, что хочу выполнить некоторые doX, если оно определено, и несколько doY, если оно не определено.Что-то вроде:

val ok = resultOpt ifSome { result =>
  println("Obtained: " + result)
  updateUIWith(result) // returns Boolean
} else {
  numInvalid += 1
  println("missing end result")
  false
}

Для меня это (все еще?) Выглядит лучше, чем полноценный match.

Я предоставляю базовую реализацию, которую придумал;Общие комментарии по этому стилю / технике и / или лучшим реализациям приветствуются!

Ответы [ 3 ]

13 голосов
/ 13 апреля 2011

Во-первых: мы, вероятно, не можем повторно использовать else, так как это ключевое слово, и использование обратных кавычек, чтобы заставить его рассматриваться как идентификатор, довольно уродливо, поэтому вместо этого я буду использовать otherwise.

Вот попытка реализации.Сначала используйте шаблон pimp-my-library для добавления ifTrue и ifFalse к Boolean.Они параметризованы для возвращаемого типа R и принимают один параметр по имени, который должен оцениваться, если указанное условие реализуется.Но при этом мы должны разрешить вызов otherwise.Таким образом, мы возвращаем новый объект с именем Otherwise0 (почему 0 объясняется позже), который сохраняет возможный промежуточный результат как Option[R].Это определяется, если текущее условие (ifTrue или ifFalse) реализовано, и в противном случае оно пустое.

class BooleanWrapper(b: Boolean) {
  def ifTrue[R](f: => R) = new Otherwise0[R](if (b) Some(f) else None)
  def ifFalse[R](f: => R) = new Otherwise0[R](if (b) None else Some(f))
}
implicit def extendBoolean(b: Boolean): BooleanWrapper = new BooleanWrapper(b)

На данный момент это работает и позволяет мне написать

someTest ifTrue {
  println("OK")
}

Но без следующего предложения otherwise, конечно, он не может вернуть значение типа R.Итак, вот определение Otherwise0:

class Otherwise0[R](intermediateResult: Option[R]) {
  def otherwise[S >: R](f: => S) = intermediateResult.getOrElse(f)
  def apply[S >: R](f: => S) = otherwise(f)
}

Он оценивает свой переданный именованный аргумент тогда и только тогда, когда промежуточный результат, который он получил из предшествующих ifTrue или ifFalse, не определен, что является именно темразыскиваетсяПараметризация типа [S >: R] приводит к тому, что S является наиболее конкретным общим супертипом фактического типа именованных параметров, например, r в этом фрагменте имеет логический тип Fruit:

class Fruit
class Apple extends Fruit
class Orange extends Fruit

val r = someTest ifTrue {
  new Apple
} otherwise {
  new Orange
}

Псевдоним apply() позволяет вообще пропустить имя метода otherwise для коротких кусков кода:

someTest.ifTrue(10).otherwise(3)
// equivalently:
someTest.ifTrue(10)(3)

Наконец, вот соответствующий сводник для Option:

class OptionExt[A](option: Option[A]) {
  def ifNone[R](f: => R) = new Otherwise1(option match {
    case None => Some(f)
    case Some(_) => None
  }, option.get)
  def ifSome[R](f: A => R) = new Otherwise0(option match {
    case Some(value) => Some(f(value))
    case None => None
  })
}

implicit def extendOption[A](opt: Option[A]): OptionExt[A] = new OptionExt[A](opt)

class Otherwise1[R, A1](intermediateResult: Option[R], arg1: => A1) {
  def otherwise[S >: R](f: A1 => S) = intermediateResult.getOrElse(f(arg1))
  def apply[S >: R](f: A1 => S) = otherwise(f)
}

Обратите внимание, что теперь нам также нужен Otherwise1, чтобы мы могли удобно передавать развернутое значение не только в аргумент функции ifSome, но и в аргумент функции otherwiseпосле ifNone.

6 голосов
/ 13 апреля 2011

Возможно, вы смотрите на проблему слишком конкретно. Вам, вероятно, будет лучше с оператором трубы:

class Piping[A](a: A) { def |>[B](f: A => B) = f(a) }
implicit def pipe_everything[A](a: A) = new Piping(a)

Теперь вы можете

("fish".length > 5) |> (if (_) println("Hi") else println("Ho"))

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

Кроме того, вы уже можете использовать опции так, как вы хотите:

Option("fish").filter(_.length > 5).
  map (_ => println("Hi")).
  getOrElse(println("Ho"))

То, что эти вещи могут принимать возвращаемое значение, не означает, что вы должны избегать их. Требуется немного привыкнуть к синтаксису; это может быть уважительной причиной для создания ваших собственных последствий. Но основная функциональность есть. (Если вы создаете свой собственный, рассмотрите вместо этого fold[B](f: A => B)(g: => B); как только вы привыкнете к нему, отсутствие ключевого слова на самом деле довольно приятно.)


Edit: хотя обозначение |> для pipe несколько стандартно, я на самом деле предпочитаю use в качестве имени метода, потому что тогда def reuse[B,C](f: A => B)(g: (A,B) => C) = g(a,f(a)) кажется более естественным.

3 голосов
/ 14 апреля 2011

Почему бы просто не использовать это так:

val idiomaticVariable = if (condition) { 
    firstExpression
  } else { 
    secondExpression 
  } 

ИМО, это очень идиоматично! :)

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...