Почему для вычислений на уровне типов требуется техника Aux?

Я почти уверен, что мне здесь чего-то не хватает, так как я новичок в Shapeless и учусь, но когда на самом деле требуется техника Aux? Я вижу, что он используется, чтобы раскрыть type оператор, подняв его до сигнатуры другого «сопутствующего» type определения.

trait F[A] { type R; def value: R }
object F { type Aux[A,RR] = F[A] { type R = RR } }

но разве это не эквивалентно простому помещению R в сигнатуру типа F?

trait F[A,R] { def value: R }
implicit def fint = new F[Int,Long] { val value = 1L }
implicit def ffloat = new F[Float,Double] { val value = 2.0D }
def f[T,R](t:T)(implicit f: F[T,R]): R = f.value
f(100)    // res4: Long = 1L
f(100.0f) // res5: Double = 2.0

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

def g[T](t:T)(implicit f: F[T], r: Blah[f.R]) ...

таким образом, мы по-прежнему вынуждены помещать дополнительный параметр типа в сигнатуру g. Используя технику Aux, мы также должны тратить дополнительное время на написание сопутствующего object. С точки зрения использования, наивному пользователю вроде меня могло бы показаться, что использование типов, зависящих от пути, вообще бесполезно.

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

Думаю, все сводится к тому, что я что-то упускаю из виду в моем простом примере.


person Edoardo Vacchi    schedule 31.12.2015    source источник


Ответы (1)


Здесь есть два отдельных вопроса:

  1. Почему в некоторых классах типов Shapeless в некоторых случаях использует члены типа вместо параметров типа?
  2. Почему Shapeless включает Aux псевдонимы типов в сопутствующие объекты этих классов типов?

Я начну со второго вопроса, потому что ответ более очевиден: псевдонимы типа Aux являются исключительно синтаксическим удобством. Вы никогда не должны их использовать. Например, предположим, что мы хотим написать метод, который будет компилироваться только при вызове с двумя списками hlists одинаковой длины:

import shapeless._, ops.hlist.Length

def sameLength[A <: HList, B <: HList, N <: Nat](a: A, b: B)(implicit
  al: Length.Aux[A, N],
  bl: Length.Aux[B, N]
) = ()

Класс типа Length имеет один параметр типа (для типа HList) и один член типа (для Nat). Синтаксис Length.Aux позволяет относительно легко ссылаться на член типа Nat в неявном списке параметров, но это просто удобство - следующее в точности эквивалентно:

def sameLength[A <: HList, B <: HList, N <: Nat](a: A, b: B)(implicit
  al: Length[A] { type Out = N },
  bl: Length[B] { type Out = N }
) = ()

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

Ответ на первый вопрос немного сложнее. Во многих случаях, включая мой sameLength, Out не имеет преимуществ быть членом типа вместо параметра типа. Поскольку Scala не позволяет использовать несколько разделов неявных параметров, нам необходимо N в качестве параметра типа для нашего метода, если мы хотим убедиться, что два Length экземпляра имеют один и тот же Out тип. В этот момент Out на Length может также быть параметром типа (по крайней мере, с нашей точки зрения, как с точки зрения авторов sameLength).

В других случаях, однако, мы можем воспользоваться тем фактом, что в Shapeless иногда (я буду говорить конкретно о where чуть позже) используются члены типа вместо параметров типа. Например, предположим, что мы хотим написать метод, который будет возвращать функцию, которая преобразует указанный тип класса case в HList:

def converter[A](implicit gen: Generic[A]): A => gen.Repr = a => gen.to(a)

Теперь мы можем использовать это так:

case class Foo(i: Int, s: String)

val fooToHList = converter[Foo]

И мы получим хороший Foo => Int :: String :: HNil. Если бы Generic Repr был параметром типа, а не членом типа, нам пришлось бы вместо этого написать что-то вроде этого:

// Doesn't compile
def converter[A, R](implicit gen: Generic[A, R]): A => R = a => gen.to(a)

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

val fooToHList = converter[Foo, Int :: String :: HNil]

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

В общем, всякий раз, когда тип однозначно определяется другими параметрами класса типа, Shapeless делает его членом типа, а не параметром типа. Каждый класс case имеет одно общее представление, поэтому Generic имеет один параметр типа (для типа класса case) и один член типа (для типа представления); каждый HList имеет одну длину, поэтому Length имеет один параметр типа, один член типа и т. д.

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

В качестве примечания, эта связь между типом «параметры» класса типа (я использую кавычки, поскольку они могут не быть параметрами в буквальном смысле Scala) называется " функциональная зависимость " на таких языках, как Haskell, но вы не должны чувствовать, что вам нужно что-то понимать о функциональных зависимостях в Haskell, чтобы понять, что происходит. на бесформенном.

person Travis Brown    schedule 31.12.2015