Какой идиоматический способ определить несколько функций как один и тот же тип в Scala? - PullRequest
0 голосов
/ 18 января 2019

Я опытный программист в ruby, python и javascript (в частности, back-end node.js), я работал в java, perl и c ++, и я использовал lisp и haskell академически, но я совершенно новый в Скала и пытается выучить некоторые условности.

У меня есть функция, которая принимает функцию в качестве параметра, аналогично тому, как функция сортировки принимает функцию компаратора. Каков идиоматический способ реализации этого?

Предположим, что эта функция принимает параметр функции y:

object SomeMath {
  def apply(x: Int, y: IntMath): Int = y(x)
}

Должно ли IntMath быть определено как trait, а различные реализации IntMath определены в разных объектах? (давайте назовем эту опцию A)

trait IntMath {
   def apply(x: Int): Int
}

object AddOne extends IntMath {
   def apply(x: Int): Int = x + 1
}

object AddTwo extends IntMath {
  def apply(x: Int): Int = x + 2
}

AddOne(1)
// => 2
AddTwo(1)
// => 3
SomeMath(1, AddOne)
// => 2
SomeMath(1, AddTwo)
// => 3

Или IntMath должен быть псевдонимом типа для сигнатуры функции? (вариант Б)

type IntMath = Int => Int

object Add {
  def one: IntMath = _ + 1
  def two: IntMath = _ + 2
}

Add.one(1)
// => 2
Add.two(1)
// => 3
SomeMath(1, Add.one)
// => 2
SomeMath(1, Add.two)
// => 3

но какой из них более идиоматичен?

Или не идиоматичны? (вариант C)

Мой предыдущий опыт работы с функциональными языками склоняет меня к B, но я никогда не видел этого раньше в scala. С другой стороны, хотя trait, кажется, добавляет ненужный беспорядок, я видел эту реализацию, и в Scala она работает намного более плавно (поскольку объект становится вызываемым с помощью функции apply).

[ Update ] Исправлен код примеров, в котором тип IntMath передается в SomeMath. Синтетический сахар, который предоставляет scala, когда object с помощью метода apply становится вызываемым как функция, создает иллюзию, что AddOne и AddTwo являются функциями и передаются как функции в варианте A.

Ответы [ 2 ]

0 голосов
/ 18 января 2019

Другой вариант - не определять что-либо и сохранять тип функции явным:

def apply(x: Int, y: Int => Int): Int = y(x)

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

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

def apply(x: MyValue, y: MyValue => MyValue2): MyValue2 = y(x)

В противном случае, B предпочтительнее, поскольку позволяет передавать функции как лямбды:

SomeMath(1, _ + 3)

A потребует от вас объявить новый экземпляр IntMath, который будет более громоздким и менее идиоматичным.

0 голосов
/ 18 января 2019

Поскольку в Scala есть явные типы функций, я бы сказал, что если вам нужно передать функцию в вашу функцию, используйте тип функции, т. Е. Ваш вариант B. Для этого они явно существуют.

Не совсем понятно, что вы имеете в виду, говоря, что вы "никогда не видели этого раньше в scala", кстати. Многие стандартные библиотечные методы принимают функции в качестве своих параметров, например, методы преобразования коллекций. Создание псевдонимов типов для передачи семантики более сложного типа также совершенно идиоматично в Scala, и на самом деле не имеет значения, является ли псевдоним типа функцией или нет. Например, один из основных типов в моем текущем проекте на самом деле является псевдонимом типа для функции, которая передает семантику с использованием описательного имени, а также позволяет легко передавать соответствующие функции без необходимости явного создания подкласса некоторой черты.

Мне также важно понять, что синтаксический сахар метода apply не имеет никакого отношения к тому, как используются функции или функциональные черты. Действительно, apply методы имеют возможность вызываться только с круглыми скобками; однако это не означает, что различные типы, имеющие apply метод, даже с одной и той же сигнатурой, являются интероперабельными , и я думаю, что эта совместимость имеет значение в этой ситуации, а также способность легко конструировать экземпляры таких типов. В конце концов, в вашем конкретном примере для вашего кода имеет значение только то, можете ли вы использовать синтаксический сахар на IntMath или нет, но для пользователей вашего кода возможность легко создать экземпляр IntMath , а также способность передавать некоторые существующие вещи, которые у них уже есть, как IntMath, гораздо важнее.

С типами FunctionN вы можете использовать синтаксис анонимной функции для создания экземпляров этих типов (на самом деле, несколько синтаксисов, по крайней мере, таких: x => y, { x => y }, _.x , method _, method(_)). До Scala 2.11 даже не было возможности создавать экземпляры типов «Single Abstract Method», и даже там требуется флаг компилятора для фактического включения этой функции. Это означает, что пользователи вашего типа должны будут написать либо:

SomeMath(10, _ + 1)

или это:

SomeMath(10, new IntMath {
  def apply(x: Int): Int = x + 1
})

Естественно, первый подход гораздо яснее.

Кроме того, типы FunctionN предоставляют единый общий знаменатель функциональных типов, что улучшает взаимодействие. Например, функции в математике не имеют какого-либо «типа», кроме их сигнатуры, и вы можете использовать любую функцию вместо любой другой функции, если их сигнатуры совпадают. Это свойство также полезно в программировании, поскольку позволяет повторно использовать определения, которые у вас уже есть, для различных целей, сокращая количество конверсий и, следовательно, возможные ошибки, которые вы можете совершать при написании этих конверсий.

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

// You might even want to extend the function type
// to improve interoperability
trait Converter[S, T] extends (S => T) {
  def apply(source: S): T
}

object Converter {
  implicit val intToStringConverter: Converter[Int, String] = new Converter[Int, String] {
    def apply(source: Int): String = source.toString
  }
}

Здесь полезно иметь фрагмент неявной области видимости, связанный с типом, потому что в противном случае пользователям Converter потребуется всегда импортировать содержимое некоторого объекта / пакета, чтобы получить неявные определения по умолчанию; однако при таком подходе все значения, определенные в object Converter, будут найдены по умолчанию.

Однако такие ситуации не очень распространены. Как правило, я думаю, вы должны сначала попытаться использовать обычный тип функции. Если вы обнаружите, что вам нужно нужен отдельный тип, по практическим причинам, только тогда вы должны создать свой собственный тип.

...