Подтвердить что ты не робот

Почему требуется техническая схема 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. С точки зрения использования, это будет выглядеть для наивного пользователя, такого как я, что нет никакой пользы в использовании зависимых от пути типов вообще.

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

Я думаю, все это сводится к тому, что я пропустил что-то в моем простом примере.

4b9b3361

Ответ 1

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

  • Почему Shapeless использует члены типа типа вместо параметров типа в некоторых случаях в некоторых классах классов?
  • Почему 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 делают наш код немного легче читать и писать, но они не меняют того, что мы можем или не можем сделать с кодом каким-либо значимым образом.

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

В других случаях, однако, мы можем воспользоваться тем фактом, что Shapeless иногда (я буду говорить конкретно о том, где в какой-то момент) использует члены типа вместо параметров типа. Например, предположим, что мы хотим написать метод, который будет возвращать функцию, которая преобразует указанный тип класса 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, чтобы получить то, что происходит в Shapeless.