Внутри Scala можно ли использовать псевдоним типа, но не разрешать перекрестное использование псевдонимов или псевдонимов типа Haskell? - программирование

Внутри 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, предполагая, что это делает то, что я думаю, что он делает?

4b9b3361

Ответ 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, не теряя информацию о типе (так что вам придется все время понижать их).