使用 squants 库,如何为方法指定特定的测量单位?

Using the squants library, how do I specify a specific unit of measure for a method?

我有一些代码滥用 Double 的方式与 String 被滥用为无类型类型的 the goto 的方式大致相同。以这种方式使用 Double 会导致许多微妙的 and/or 隐藏问题,通常作为有害的运行时错误出现。这就是这两种方法现在的样子:

object Geospatial {
  def calculateDistance(
    coordinate1Longitude: Double,
    coordinate1Latitude: Double,
    coordinate2Longitude: Double,
    coordinate2Latitude: Double
  ): (Double, Double, Double) = {
    //code encapsulated here only works with meters and radians
    //returns (distance in meters, initial bearing in zero-based radians, final bearing in zero-based radians)
    ???
  }

  def calculateCoordinate(
    coordinate1Longitude: Double,
    coordinate1Latitude: Double,
    angle: Double,
    distance: Double
  ): (Double, Double) = {
    //code encapsulated here only works with meters and radians
    //returns angle in radians for longitude and latitude
    ???
  }
}

正如您想象的那样,如果客户端要调用这些方法中的任何一个并且尚未正确转换为米和弧度 and/or 还忘记了这些方法以米和弧度为单位返回值,客户端会得到不正确的结果。

所以,我想显着提高上述方法的类型安全性;即我希望客户端收到编译时错误 if/when 客户端尝试调用这些方法中的任何一个,而它们传递的类型与这些方法所需的类型不完全匹配。本着这种精神,我重写了方法(及其上下文)以使其更加类型化(但仍然不够):

object Geospatial {
  type AngleRadiansCentered = Double
    //angle's range is restricted to [-Math.PI until Math.PI]
  type AngleRadiansPositive = Double
    //angle's range is restricted to [0.0d until (Math.PI * 2.0d)]

  type LongitudeRadians = AngleRadiansCentered
  type LatitudeRadians = AngleRadiansCentered  //angle's range must be _further_ restricted to -(Math.PI / 2.0d) until (Math.PI / 2.0d)

  def calculateDistance(
    coordinate1: (Longitude, Latitude),
    coordinate2: (Longitude, Latitude)
  ): (Meters, AngleRadiansPositive, AngleRadiansPositive) = {
    //Legacy code encapsulated here only works with meters and radians
    //returns (distance, initial bearing, final bearing)
    ???
  }

  def calculateCoordinate(
    coordinate1: (Longitude, Latitude),
    bearing: AngleRadiansPositive,
    distance: Meters
  ): (Longitude, Latitude) = {
    //Legacy code encapsulated here only works with meters and radians
    ???
  }
}

因为我一直在解决这个问题,我最近发现了 squants library. I'm thinking I would like to rewrite the above code using squants. However, after spending an hour or so reading the sparse squants 文档(至少是关于我的上下文的相关示例),我无法在如何将它应用到这个问题上做出任何合理的飞跃问题。例如,我如何将它指定为只接受 Meters(而不是更通用的 Length)的方法参数。 IOW,我正在寻找类型安全,而不是类型之间的转换(尽管这是我将在这些方法之外做的事情)。

我找不到任何 squants example code snippits from which I could derive what I'm needing. And I'm not asking for the full solution. I just need to be pointed in the right direction. First, I need to know if squants 是真正适合使用的 API。然后,如果是的话,我需要足够的帮助来推动我朝着正确的总体方向前进,这样我才能找出解决方案的其余部分space。

我确实打算至少用案例 类 来替换 Double 的所有实例。不过,在我这样做之前,我想知道是否有惯用的方法可以使用 squants 库来做到这一点。

任何关于这方面的指导将不胜感激。

我经历了同样的过程,这就是我的发现:squants 库,至少就其本身而言,不会 完全 提供您正在寻找的内容。它提供的类型安全,特指不同类型的数量不混用,但同一维度内的实际单位并不重要。

也就是说,在使用它一段时间后,我意识到它采用的方法实际上是满足我需要的正确方法。您仍然可以获得重要的类型安全:将值保持在其维度内。在同一维度内混合不同的单位仍然是安全的,因为您创建 Length 单位的方式是使用 "constructors" 之一,例如 MetersCentimeter。内部表示应该无关紧要。您正在使用 Length(重要部分)。如果您想要原始 Meter(保存到数据库?),请在那时调用 toMeters。这样做不会损失类型安全性。

感谢 , I was able to see my initial approach to use the squants library was slightly askew (located on pastebin.com,已修复)。下面是具体解决方案最终的样子:

import squants.space.{Radians, Meters}
import squants.{Angle, Length}

object Geospatial {
  case class Longitude(angle: Angle) {
    require(
      (Radians(-Math.PI) <= angle) && (angle < Radians(Math.PI)),
      "angle.inRadians must be greater than or equal to -Math.PI and less than Math.PI"
    )
  }
  case class Latitude(angle: Angle) {
    require(
      (Radians(-(Math.PI * 0.5d)) <= angle) && (angle < Radians(Math.PI * 0.5d)),
      "angle.inRadians must be greater than or equal to -(Math.PI * 0.5d) and less than (Math.PI * 0.5d)"
    )
  }
  case class Distance(length: Length) {
    require(
      Meters(0.0d) <= length,
      "length.inMeters must be greater than or equal to 0.0d"
    )
  }
  case class Bearing(angle: Angle) {
    require(
      (Radians(0.0d) <= angle) && (angle < Radians(Math.PI * 2.0d)),
      "angle.inRadians must be greater than or equal to 0.0d and less than (Math.PI * 2.0d)"
    )
  }

  case class Coordinate(longitude: Longitude, latitude: Latitude)

  def calculateDistance(
    coordinate1: Coordinate,
    coordinate2: Coordinate
  ): (Distance, Bearing, Bearing) = {
    def calculateDistanceUsingLegacyCodeRifeWithDoubles(
      coordinate1LongitudeInRadians: Double,
      coordinate1LatitudeInRadians: Double,
      coordinate2LongitudeInRadians: Double,
      coordinate2LatitudeInRadians: Double
    ): (Double, Double, Double) = {
      //Legacy code encapsulated here only works with meters and radians
      //returns (distance, initial bearing, final bearing)
      (1.0d, 1.0d, 2.0d) //TODO: replace with real calculation results
    }
    val (coordinate1InRadians, coordinate2InRadians) = (
      (coordinate1.longitude.angle.toRadians, coordinate1.latitude.angle.toRadians),
      (coordinate2.longitude.angle.toRadians, coordinate2.latitude.angle.toRadians)
    )
    val (distanceInMeters, bearingInitialInRadians, bearingFinalInRadians) =
      calculateDistanceUsingLegacyCodeRifeWithDoubles(
        coordinate1InRadians._1,
        coordinate1InRadians._2,
        coordinate2InRadians._1,
        coordinate2InRadians._2
      )
    (
      Distance(Meters(distanceInMeters)),
      Bearing(Radians(bearingInitialInRadians)),
      Bearing(Radians(bearingFinalInRadians))
    )
  }

  def calculateCoordinate(
    coordinate1: Coordinate,
    bearingInitial: Bearing,
    distance: Distance
  ): Coordinate = {
    def calculateCoordinateUsingLegacyCodeRifeWithDoubles(
      coordinate1Longitude: Double,
      coordinate1Latitude: Double,
      bearingInitialInRadians: Double,
      distanceInMeters: Double
    ): (Double, Double) = {
      //Legacy code encapsulated here only works with meters and radians
      //returns (longitude, latitude)
      (-1.0d, 1.0d) //TODO: replace with real calculation results
    }
    val (coordinate1InRadians, bearingInitialInRadians, distanceInMeters) = (
      (coordinate1.longitude.angle.toRadians, coordinate1.latitude.angle.toRadians),
      bearingInitial.angle.toRadians,
      distance.length.toMeters
    )
    val (longitude, latitude) =
      calculateCoordinateUsingLegacyCodeRifeWithDoubles(
        coordinate1InRadians._1,
        coordinate1InRadians._2,
        bearingInitialInRadians,
        distanceInMeters
      )
    Coordinate(Longitude(Radians(longitude)), Latitude(Radians(latitude)))
  }
}