Ответ 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.