Внутри Scala можно ли использовать псевдоним типа, но не разрешать перекрестное использование псевдонимов или псевдонимов типа Haskell?
В Haskell я считаю, что можно использовать псевдоним типа таким образом, чтобы компилятор не разрешал ссылки между псевдонимом и неустановленным типом. Согласно этому вопросу, можно использовать Haskell newtype
следующим образом:
newtype Feet = Feet Double
newtype Cm = Cm Double
где Feet
и Cm
будут вести себя как двойные значения, но попытка умножить значение Feet
и значение Cm
приведет к ошибке компилятора.
EDIT: Бен отметил в комментариях, что этого выше определения в Haskell недостаточно. Feet
и Cm
будут новые типы, на которых не будет определено никаких функций. Сделав еще несколько исследований, я обнаружил, что будут работать следующие:
newtype Feet = Feet Double deriving (Num)
newtype Cm = Cm Double deriving (Num)
Это создает новый тип, который происходит из существующего типа Num
(требуется использовать переключатель: -XGeneralizedNewtypeDeriving
). Конечно, эти новые типы будут еще более ценными из других типов, таких как Show
, Eq
и т.д., Но это минимум, необходимый для правильной оценки Cm 7 * Cm 9
.
Оба Haskell и Scala имеют type
, который просто псевдонизирует существующий тип и допускает бессмысленный код, такой как этот пример, в Scala:
type Feet = Double
type Cm = Double
val widthInFeet: Feet = 1.0
val widthInCm: Cm = 30.48
val nonsense = widthInFeet * widthInCm
def getWidthInFeet: Feet = widthInCm
Имеет ли Scala эквивалент newtype
, предполагая, что это делает то, что я думаю, что он делает?
Ответы
Ответ 1
Да, вы используете что-то известное как Unboxed Tagged Types в scala.
Это определяется как Tagged:
type Tagged[U] = { type Tag = U }
type @@[T, U] = T with Tagged[U]
Это позволяет вам сделать что-то вроде этого
sealed trait Feet
def Feet[A](a: A): A @@ Feet = Tag[A, Feet](a)
Feet: [A](a: A)[email protected]@[A,Feet]
scala> val mass = Feet(20.0)
mass: [email protected]@[Double,Feet] = 20.0
scala> 2 * mass
res2: Double = 40.0
чтобы добавить CM
sealed trait CM
def CM[A](a: A): A @@ CM = Tag[A, CM](a)
CM: [A](a: A)[email protected]@[A,CM]
scala> val mass = CM(20.0)
mass: [email protected]@[Double,CM] = 20.0
Если вы хотите ограничить умножение только на Feet, тогда вы можете написать функцию умножения типа типа
trait Multiply[T] { self =>
def multiply(a: T, b: T): T
}
implicit val footInstance = new Multiply[Feet] {
def multiply(a: Feet, b: Feet): Feet = Feet(a * b)
}
implicit val cmInstance = new Multiply[CM] {
def multiply(a: CM, b: CM): CM = CM(a * b)
}
def multiply[T: Multiply](a: T, b: T): T = {
val multi = implicitly[Multiply[T]]
multi.multiply(a,b)
}
вы можете сделать
multiply(Feet(5), Feet(10)) // would return Feet(50)
это лучший Scala может делать
Чтобы узнать больше о проверке в штучной упаковке
http://eed3si9n.com/learning-scalaz-day3
Ответ 2
Другой вариант - использовать классы значений. Они создают оболочку вокруг базового типа, которая преобразуется в прямой доступ к необработанному типу во время компиляции, при этом методы класса преобразуются в статические вызовы связанного сопутствующего объекта. Например:
class CM(val quant : Double) extends AnyVal {
def +(b : CM) = new CM(quant + b.quant)
def *(b : Int) = new CM(quant * b)
}
Ответ 3
Вы можете использовать NewType
из библиотеки scala-newtype!
Бесстыдный плагин: Я автор scala-newtype
https://github.com/estatico/scala-newtype
Это объединяет идеи из Scalaz и Shapeless, а также представляет идеи прямо из Haskell (например, GeneralizedNewTypeDeriving).
Вот как может выглядеть ваш код при использовании newtype. Мы предоставим как Feet
, так и Cm
их собственные различные типы, и пусть они получат класс типов Numeric
на основе класса Double (который deriving
делает автоматически).
Затем мы можем использовать методы расширения, предоставленные Numeric.Implicits
-
object Example {
type Feet = Feet.Type
object Feet extends NewType.Default[Double] {
implicit val num: Numeric[Type] = deriving
}
type Cm = Cm.Type
object Cm extends NewType.Default[Double] {
implicit val num: Numeric[Type] = deriving
}
val widthInFeet = Feet(1.0)
val widthInCm = Cm(30.48)
import Numeric.Implicits._
// Does not compile:
// val nonsense = widthInFeet + widthInCm
// Compiles!
val doubleWidthInFeet: Feet = widthInFeet + widthInFeet
}
Тем не менее, вы используете *
в этом примере, и мы бы не хотели, чтобы Feet * Feet = Feet
был действительно Feet * Feet = Feet²
, поэтому давайте добавим тип FeetSq
, чтобы представить это и определить наши собственные операции, которые будут более безопасный тип, чем Numeric
-
object Example {
type Feet = Feet.Type
object Feet extends NewType.Default[Double] {
implicit final class Ops(val self: Feet) extends AnyVal {
def +(other: Feet) = Feet(self.repr + other.repr)
def -(other: Feet) = Feet(self.repr - other.repr)
def *(other: Feet) = FeetSq(self.repr * other.repr)
}
}
type FeetSq = FeetSq.Type
object FeetSq extends NewType.Default[Double]
type Cm = Cm.Type
object Cm extends NewType.Default[Double]
val widthInFeet = Feet(1.0)
val widthInCm = Cm(30.48)
// Does not compile:
// val nonsense = widthInFeet * widthInCm
// Compiles!
val squareFeet: FeetSq = widthInFeet * widthInFeet
}
Здесь мы используем implicit final class Ops
для определения методов нашего нового типа. Этот класс исключается во время компиляции, поэтому во время выполнения мы просто в конечном итоге вызываем методы расширения из объекта Ops
.
Ответ 4
Для
val widthInCm: Cm = 30.48
def getWidthInFeet: Feet = widthInCm
просто определяя Feet
и Cm
следующим образом:
type Feet <: Double
type Cm <: Double
выполнит эту работу, если вы не вернете их обратно к Double
. Смотрите сами:
def getWidthInFeet: Feet = widthInCm
Error:(1, 28) type mismatch;
found : widthInCm.type (with underlying type Cm)
required: Feet
def getWidthInFeet: Feet = widthInCm
Побочным эффектом является то, что вам необходимо явно уменьшить значения Double
до Cm
или Feet
, чтобы получить экземпляр:
val widthInCm: Cm = 30.48.asInstanceOf[Cm]
Кроме того, вы не можете выполнять над ними никакую операцию Double
, не теряя информацию о типе (так что вам придется все время понижать их).