Статьи

Scala: Вы частично это понимаете?

Почти каждый, кто изучает Scala, может запутаться из-за слова, которое используется в контексте:

  • Частичные функции
  • Частично примененные функции 

Давайте посмотрим на оба.

Частично примененные функции


Scala получает свои функциональные идеи от классических языков, таких как Haskell (Haskell 1.0 появился в том же году, что и Depeche Mode, «
Enjoy the Silence», а « Groove» от Dee Lite
в сердце в 1990 году). В функциональных языках функция, которая принимает два параметра и возвращает один параметр, может быть выражена как функция, которая принимает один из входных параметров и возвращает функцию, которая принимает другой входной параметр и возвращает тот же выходной параметр.

 f(x1, x2) = y
f(x1) = f(x2) = y


Подходящей аналогией было бы путешествие во времени в 1990-е годы и поиск себя в Juxebox.
Положите деньги на два выбора и сначала выберите Depeche Mode, а затем Dee Lite, отойдите и бросьте несколько фигур, пока они играются одна за другой. Или, вложив свои деньги на два выбора, выберите Depeche Mode, а затем не делайте другого выбора. Пока не уходи. Хорошо спроектированный Juxebox должен предложить вам другой выбор (дать вам другую функцию), а затем вы можете выбрать Dee Lite (передать второй параметр). Таким образом, конечный результат в обоих случаях — одна и та же музыка в одном и том же порядке.

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


Итак, рассмотрим функцию:

def minus(a: Int, b: Int) = "answer=" + (a-b)

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

val minus50 = (a: Int) => minus(a, 50);


В этом случае
minus50является
частичным применением минуса.

Мы сможем:

minus50(57); // outputs 7.
Примечание: мы также можем частично подать заявку, используя нотацию _ и сэкономить немного отпечатков пальцев.

val minus50 = minus(_:Int, 50);

Частичные функции

Частичная функция — это функция, которая действует только для подмножества значений тех типов, которые вы можете передать в нее. Например, рассмотрим математическую функцию, где x задается из всех чисел от 1 до 100: 


f (x) = x + 5;

Функция называется частично примененной, если функция применяется только к подмножеству в наборе элементов x.

Так что, если мы хотим определить только функцию

f(x) = x + 5


для чисел 1,2,3,4,5,6,7, но не для 8,9,10, … — мы определяем частичную функцию.
f(x')=x+5

где x ‘= {1,2,3,4,5,6,7}

В Scala функция PartialFunction наследуется от Function и добавляет два интересных метода:

  • isDefinedAt — это позволяет нам проверить, определено ли значение для частичной функции.
  • orElse— это позволяет объединять частичные функции. Таким образом, если значение не определено для функции, оно может быть передано другой функции. Это похоже на шаблон цепочки ответственности GoF .

Итак, откройте Scala REPL и создайте следующую частичную функцию, которая добавит 5 к целому числу, если целое число меньше 7.

val add5Partial : PartialFunction[Int, Int] = {
  case d if (d > 0) && (d <= 7) => d + 5;
}

Если вы попробуете это для значения, меньшего или равного 7, вы увидите результат без проблем

scala > add5Partial(6);
res1: 11

Когда вы пробуете значение больше 7, вы не получите хороший чистый ответ.

scala> myPartial(42);
scala.MatchError: 42 (of class java.lang.Integer)
        at $anonfun$1.apply$mcII$sp(<console>:7)
        at .<init>(<console>:9)
        at .<clinit>(<console>)
        at .<init>(<console>:11)
        at .<clinit>(<console>)
        at $print(<console>)
        at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.lang.reflect.Method.invoke(Method.java:601)
        at scala.tools.nsc.interpreter.IMain$ReadEvalPrint.call(IMain.scala:704)
        at scala.tools.nsc.interpreter.IMain$Request$$anonfun$14.apply(IMain.scala:920)
        at scala.tools.nsc.interpreter.Line$$anonfun$1.apply$mcV$sp(Line.scala:43)
        at scala.tools.nsc.io.package$$anon$2.run(package.scala:25)
        at java.lang.Thread.run(Thread.java:722)

Использование
isDefinedAt()должно теперь стать очевидным. В этом случае мы могли бы сделать:

add5Partial.isDefinedAt(4)
res3: Boolean = true
 
scala> add5Partial.isDefinedAt(42)
res4: Boolean = false

Хорошо, а что
orElse? Давайте определим еще одну частичную функцию, которая работает с числами больше 7 и меньше 100. В таких случаях давайте просто добавим 4.

val add4Partial : PartialFunction[Int, Int] = {
  case d if (d > 7) && (d <= 100) => d + 5;
}

Теперь мы можем просто сделать:

scala> val addPartial = add5Partial orElse add4Partial;
addPartial : PartialFunction[Int,Int] = <function1>
scala> addPartial(42);
res6: Int = 46

Хорошо, давайте посмотрим, как все это может быть реализовано в Java с использованием шаблона Chain of Responsibility. Во-первых, давайте определим интерфейс обработчика и реализацию add5 и add4, которая будет его реализовывать.

//Handler
public interface AdditionHandler {
    //reference to the next handler in the chain
    public void setNext(AdditionHandler handler);
    //handle request
    public void handleRequest(int number);
}
 
public class Add5Handler implements AdditionHandler {
    private AdditionHandler nextAdditionHandler = null;
    public void setNext(AdditionHandler hander)  {
        this.nextAdditionHandler = handler;
    }
 
    public int handleRequest(int number) {
         if ((number > 0) && (number <= 7)) {
             return number + 5;
         } else {
             return nextAdditionHandler.handleRequest(number);
         }
    }
}
 
public class Add4Handler implements AdditionHandler {
    private AdditionHandler nextAdditionHandler = null;
    public void setNext(AdditionHandler hander)  {
        this.nextAdditionHandler = handler;
    }
 
    public int handleRequest(int number) {
         if ((number > 7) && (number <= 100)) {
             return number + 4;
         } else {
             return nextAdditionHandler.handleRequest(number);
         }
    }   
}

Теперь давайте создадим класс, который будет связывать обработчики.

public class AdditionProcessor {
   private AdditionHandler prevHandler;
   public void addHandler(AdditionHandler handler){
       if(prevHandler != null) {
           prevHandler.setNext(handler);
       }
       prevHandler = handler;
   }
}

И, конечно же, клиент, который на самом деле вызывает функциональность:

public class AdditionClient {
    private AdditionProcessor processor;
    public AdditionClient(){
        createProcessor();
    }
 
    private void createProcessor() {
        processor = new AdditionProcessor();
        processor.addHandler(new Add5Handler());
        processor.addHandler(new Add4Handler());
    }
 
    public void addRule(AdditionHandler handler) {
        processor.addHandler(handler);
    }
 
    public void requestReceived(int value){
        System.out.println("value=" + processor.handleRequest(value)); 
    }
 
    public static void main(String[] args) {
        AdditionClient client = new AdditionClient();
 
    }
}

Так что у Scala есть некоторые явные преимущества. Или, конечно, люди скажут «
ах, но на Java вы просто делаете …»

public int addFunction(int value) {
    if ((value > 0) && (value <= 7)) {
       return value + 5;
    } else if ((value > 7) && (value < 100)) {
       return value + 4;
    } else {
      // ...
    }
}

И да, для этого конкретного случая это будет работать. Но что, если ваши функции / команды станут более сложными. Вы собираетесь торчать на
if / elseземле? Возможно нет. «До следующего раза, береги себя.