有没有办法在 OCaml 类型系统中嵌入单元处理逻辑?

如何解决有没有办法在 OCaml 类型系统中嵌入单元处理逻辑?

这可能是不可能的,但我觉得我可能会学到一些东西,或者可能有一种截然不同的方法来处理它。

我正在编写一些包含一些物理模拟元素的代码,我可能要处理一堆不同的单元。我觉得让类型系统在这方面为我做一些工作是值得的,这样我就不能,例如,为距离增加质量或类似的东西。

那很容易:

module Mass : sig 
  type t
  val sum : t -> t -> t
  ...
end = struct
  type t = int
  let sum = +
  ...
end
module Distance : sig 
  type t
  val sum : t -> t -> t
  ...
end = struct
  type t = int
  let sum = +
  ...
end

现在编译器会阻止我尝试混合两者(对吗?)。类似的东西也应该用于表达相同类型的单位(例如,磅与公斤),甚至可能使我免受一些精度或溢出错误的影响。到目前为止很容易。有趣/困难的部分是我想制作一个流畅的框架来处理单位的组合,比如米每秒平方等。

我可以通过玩仿函数来接近:

module MakeProduct(F : UnitT)(S: UnitT) = struct
   type t = (F.t,S.t)
   ...
end
module MakeRatio(Numerator : UnitT)(Denominator: UnitT) = struct
   type t = (Numerator.t,Denominator.t)
   ...
end

然后我就可以了

module MetersPerSecondSquared = MakeRatio(MakeRatio(Meter)( Second))(Second)

会有一些非常笨拙的函数名称,但这应该会给我一个类型安全的系统,我可以在其中将 25 m/s^2 乘以 5s 并得到 125m/s.

我看到的问题,除了笨拙之外,是系统将无法识别以不同顺序表达相同事物的类型。例如,我也可以将上述内容表达为:

MakeRatio(Meter)(Product(Second)(Second))

两者最终都应该表达相同的概念,但我不知道有什么方法可以告诉类型系统它们是相同的,或者您仍然应该能够将第二个乘以 5s 并得到m/s 中的结果。

我正在努力学习的是:

  1. 到底有没有办法让这个想法奏效?
  2. 如果没有,是否有正式/理论上的原因这很难? (仅用于我自己的教育)
  3. 是否有一种完全不同的方式来干净地处理类型系统中的不同单元?

解决方法

可以使用正确的编码进行一些有限的类型级算术。然而,任何编码都会受到OCaml类型系统不知道算术这一事实的限制,并且不能被自己欺骗来证明复杂的算术定理。

一种可能在复杂性和特征之间取得良好折衷的编码是使用一组固定的核心单元(例如 mskg)和一个幻像类型描述浮点数的单位。

module Units: sig
  type 'a t
  val m: <m: ?one; s: ?zero; kg: ?zero> t
end = struct
  type 'a t = float
  let m = 1.
end

这里的类型 <m:'m; s:'s; kg:'kg> Units.t 本质上是一个浮点数,增加了一些类型参数 <m:'m; s:'s; kg:'kg>,描述了每个基本单位的单位指数。

我们希望这个指数是一个类型级别的整数(所以 ?zero 应该是 0 等的类型级别编码......)。

整数的一种有用编码是将它们编码为翻译而不是在一元整数之上。 首先,我们可以在类型级别定义一个一元 z (for zero) 类型和一个后继函数:

type z = Zero
type 'a succ = S

然后我们可以将 zero 编码为将整数 n 映射到 n 的函数:

type 'n zero = 'n * 'n

one 作为将整数 n 映射到 n + 1 的函数:

type 'n one = 'n * 'n succ

通过这种编码,我们可以在 ?zero 模块中填写 ?oneUnit 占位符:

module Unit: sig
  type +'a t

  (* Generators *)
  val m: <m:_ one; s:_ zero; kg:_ zero> t
  val s: <m:_ zero; s:_ one; kg:_ zero> t
  val kg: <m:_ zero; s:_ zero; kg:_ one> t
  ...
end

然后我们可以使用我们的翻译编码来欺骗类型检查器通过类型统一来计算加法:

  val ( * ):
    <m:'m_low * 'm_mid; s:'s_low * 's_mid; kg:'kg_low * 'kg_mid>  t ->
    <m:'m_mid * 'm_high; s:'s_mid * 's_high; kg:'kg_mid * 'kg_high>  t ->
    <m:'m_low * 'm_high; s:'s_low * 's_high; kg:'kg_low * 'kg_high>  t

在这里,如果我们查看每个组件上发生的事情,我们实际上是在说明如果我们有一个从 'm_low'm_mid 的翻译以及另一个从 'm_mid 到 {{ 的翻译1}},这两个翻译的总和就是从 m_high'm_low 的翻译。因此,我们在类型级别实现了加法。

把所有东西放在一起,我们最终得到

'm_high

然后我们得到了预期的行为:只有具有相同维度的值才能相加(或相减),乘法值是通过相加维度分量来完成的(除法相反)。例如,此代码正确编译

module Unit: sig
  type +'a t

  (* Generators *)
  (* Floats are dimensionless *)
  val scalar: float -> <m:_ zero; s: _ zero; kg: _ zero> t
  val float: <m:_ zero; s: _ zero; kg: _ zero> t -> float
  (* Base units *)
  val m: <m:_ one; s:_ zero; kg:_ zero> t
  val s: <m:_ zero; s:_ one; kg:_ zero> t
  val kg: <m:_ zero; s:_ zero; kg:_ one> t

  (* Arithmetic operations *)
  val ( + ): 'a t -> 'a t -> 'a t
  val ( * ):
    <m:'m_low * 'm_mid; s:'s_low * 's_mid; kg:'kg_low * 'kg_mid>  t ->
    <m:'m_mid * 'm_high; s:'s_mid * 's_high; kg:'kg_mid * 'kg_high>  t ->
    <m:'m_low * 'm_high; s:'s_low * 's_high; kg:'kg_low * 'kg_high>  t

  val ( / ) :
    <m:'m_low * 'm_high; s:'s_low * 's_high; kg:'kg_low * 'kg_high> t ->
    <m:'m_mid * 'm_high; s:'s_mid * 's_high; kg:'kg_mid * 'kg_high> t ->
    <m:'m_low * 'm_mid ; s:'s_low * 's_mid ; kg:'kg_low * 'kg_mid > t
end = struct
 type +'a t = float
 let scalar x = x let float x = x
 let ( + ) = ( +. ) let ( * ) = ( *. ) let ( / ) = ( /. )
 let m = 1. let s = 1. let kg = 1.
end

而试图将天文单位添加到一年会产生类型错误

open Units
let ( *. ) x y = scalar x * y
let au = 149_597_870_700. *. m
let c  = 299_792_458. *. m / s
let year = 86400. *. (365. *. s)
let ok = float @@ (c * year) / au

错误:这个表达式有类型 Unit.t 但预期类型为表达式 Unit.t 类型变量 'b 出现在 'b succ

然而,类型错误并不是真正可以理解的......这是使用编码的常见问题。

这种编码的另一个重要限制是我们使用类型变量的统一来进行计算。通过这样做,只要类型变量没有被泛化,我们就会在进行计算时消耗它。这会导致奇怪的错误。例如,这个功能工作正常

let error = year + au

而这个没有类型检查

let strange_but_ok x y = m * x +  ((y/m) * m) * m

幸运的是,由于我们的幻像类型参数是协变的,放宽的值限制将确保大部分时间类型变量按时泛化;并且只有在将不同维度的函数参数混合在一起时才会出现问题。

这种编码的另一个重要限制是我们仅限于单位的加法、减法、乘法和除法。例如,不可能用这种表示计算平方根。

解除这个限制的一种方法是仍然对单位使用幻影类型参数,使用类型构造函数表示加法、乘法等,并在这些类型构造函数之间添加一些公理等式。但随后用户必须手动证明同一整数的不同表示之间的等价性。

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


使用本地python环境可以成功执行 import pandas as pd import matplotlib.pyplot as plt # 设置字体 plt.rcParams[&#39;font.sans-serif&#39;] = [&#39;SimHei&#39;] # 能正确显示负号 p
错误1:Request method ‘DELETE‘ not supported 错误还原:controller层有一个接口,访问该接口时报错:Request method ‘DELETE‘ not supported 错误原因:没有接收到前端传入的参数,修改为如下 参考 错误2:cannot r
错误1:启动docker镜像时报错:Error response from daemon: driver failed programming external connectivity on endpoint quirky_allen 解决方法:重启docker -&gt; systemctl r
错误1:private field ‘xxx‘ is never assigned 按Altʾnter快捷键,选择第2项 参考:https://blog.csdn.net/shi_hong_fei_hei/article/details/88814070 错误2:启动时报错,不能找到主启动类 #
报错如下,通过源不能下载,最后警告pip需升级版本 Requirement already satisfied: pip in c:\users\ychen\appdata\local\programs\python\python310\lib\site-packages (22.0.4) Coll
错误1:maven打包报错 错误还原:使用maven打包项目时报错如下 [ERROR] Failed to execute goal org.apache.maven.plugins:maven-resources-plugin:3.2.0:resources (default-resources)
错误1:服务调用时报错 服务消费者模块assess通过openFeign调用服务提供者模块hires 如下为服务提供者模块hires的控制层接口 @RestController @RequestMapping(&quot;/hires&quot;) public class FeignControl
错误1:运行项目后报如下错误 解决方案 报错2:Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.8.1:compile (default-compile) on project sb 解决方案:在pom.
参考 错误原因 过滤器或拦截器在生效时,redisTemplate还没有注入 解决方案:在注入容器时就生效 @Component //项目运行时就注入Spring容器 public class RedisBean { @Resource private RedisTemplate&lt;String
使用vite构建项目报错 C:\Users\ychen\work&gt;npm init @vitejs/app @vitejs/create-app is deprecated, use npm init vite instead C:\Users\ychen\AppData\Local\npm-
参考1 参考2 解决方案 # 点击安装源 协议选择 http:// 路径填写 mirrors.aliyun.com/centos/8.3.2011/BaseOS/x86_64/os URL类型 软件库URL 其他路径 # 版本 7 mirrors.aliyun.com/centos/7/os/x86
报错1 [root@slave1 data_mocker]# kafka-console-consumer.sh --bootstrap-server slave1:9092 --topic topic_db [2023-12-19 18:31:12,770] WARN [Consumer clie
错误1 # 重写数据 hive (edu)&gt; insert overwrite table dwd_trade_cart_add_inc &gt; select data.id, &gt; data.user_id, &gt; data.course_id, &gt; date_format(
错误1 hive (edu)&gt; insert into huanhuan values(1,&#39;haoge&#39;); Query ID = root_20240110071417_fe1517ad-3607-41f4-bdcf-d00b98ac443e Total jobs = 1
报错1:执行到如下就不执行了,没有显示Successfully registered new MBean. [root@slave1 bin]# /usr/local/software/flume-1.9.0/bin/flume-ng agent -n a1 -c /usr/local/softwa
虚拟及没有启动任何服务器查看jps会显示jps,如果没有显示任何东西 [root@slave2 ~]# jps 9647 Jps 解决方案 # 进入/tmp查看 [root@slave1 dfs]# cd /tmp [root@slave1 tmp]# ll 总用量 48 drwxr-xr-x. 2
报错1 hive&gt; show databases; OK Failed with exception java.io.IOException:java.lang.RuntimeException: Error in configuring object Time taken: 0.474 se
报错1 [root@localhost ~]# vim -bash: vim: 未找到命令 安装vim yum -y install vim* # 查看是否安装成功 [root@hadoop01 hadoop]# rpm -qa |grep vim vim-X11-7.4.629-8.el7_9.x
修改hadoop配置 vi /usr/local/software/hadoop-2.9.2/etc/hadoop/yarn-site.xml # 添加如下 &lt;configuration&gt; &lt;property&gt; &lt;name&gt;yarn.nodemanager.res