【问题标题】:Is there a language with constrainable types?是否存在具有可约束类型的语言?
【发布时间】:2013-10-11 07:40:52
【问题描述】:

有没有一种类型化的编程语言可以像下面两个例子那样约束类型?

  1. 概率是一个浮点数,最小值为 0.0,最大值为 1.0。

    type Probability subtype of float
    where
        max_value = 0.0
        min_value = 1.0
    
  2. 离散概率分布是一个映射,其中:键应该都是同一类型,值都是概率,值的总和 = 1.0。

    type DPD<K> subtype of map<K, Probability>
    where
        sum(values) = 1.0
    

据我了解,这对于 Haskell 或 Agda 是不可能的。

【问题讨论】:

标签: haskell agda dependent-type


【解决方案1】:

你想要的是refinement types

可以在 Agda 中定义ProbabilityProb.agda

概率质量函数类型,求和条件在第 264 行定义。

与 Agda 相比,有些语言具有更直接的细化类型,例如 ATS

【讨论】:

  • 我在 Agda 或 Coq 中所做的与此问题所要求的区别在于,细化类型是 new 类型,而不是现有类型的子类型。例如,DPD 将是一个包含映射和一些证明的新类型,而不是恰好满足某些附带条件的映射。
  • @пропессор 谢谢---答案已接受!我以为阿格达能做到。不幸的是,我发现即使是最简单的 Agda 也难以理解(我只在 Haskell 的苗圃斜坡上)。 ATS 看起来很有趣:我会尝试一下。
  • @Antal S-Z 不应过分强调伪代码中的“子类型”。我可以很容易地写出“细化”。
【解决方案2】:

您可以在 Haskell 中使用 Liquid Haskell 执行此操作,它使用 refinement types 扩展 Haskell。谓词在编译时由 SMT 求解器管理,这意味着证明是全自动的,但您可以使用的逻辑受到 SMT 求解器处理的限制。 (令人高兴的是,现代 SMT 求解器相当通用!)

一个问题是我认为 Liquid Haskell 目前不支持浮点数。如果不是这样,应该可以纠正,因为有 用于 SMT 求解器的浮点数理论。您也可以假装浮点数实际上是有理数(甚至在 Haskell 中使用 Rational!)。考虑到这一点,您的第一个类型可能如下所示:

{p : Float | p >= 0 && p <= 1}

您的第二种类型会更难编码,特别是因为地图是一种难以推理的抽象类型。如果您使用配对列表而不是地图,则可以这样编写“度量”:

measure total :: [(a, Float)] -> Float
total []          = 0 
total ((_, p):ps) = p + probDist ps

(您可能也想将[] 包装在newtype 中。)

现在您可以在细化中使用total 来约束列表:

{dist: [(a, Float)] | total dist == 1}

Liquid Haskell 的巧妙之处在于,所有推理都是在编译时为您自动执行的,以换取使用某种受限的逻辑。 (total 之类的度量在编写方式上也受到很大限制——它是 Haskell 的一个小子集,具有“每个构造函数只有一个案例”之类的规则。)这意味着这种风格的细化类型不太强大,但更容易使用比完全依赖的类型更实用。

【讨论】:

  • 感谢 HT!碰巧的是,我们最近添加了对这类事情的支持,请参阅:github.com/ucsd-progsys/liquidhaskell/blob/master/tests/pos/…
  • @RanjitJhala:这种东西是浮点理论吗?还是更像实数?
  • @RanjitJhala,并非所有这些实际上都适用于浮点。 inverse 尤其没有。
  • 确实,LH 使用了 SMT 求解器的实数理论(不是浮点数)。
【解决方案3】:

Perl6 有一个“类型子集”的概念,它可以添加任意条件来创建“子类型”。

专门针对您的问题:

subset Probability of Real where 0 .. 1;

role DPD[::T] {
  has Map[T, Probability] $.map
    where [+](.values) == 1; # calls `.values` on Map
}

(注意:在当前的实现中,“where”部分是在运行时检查的,但是因为“真实类型”是在编译时检查的(包括你的类),并且因为有纯注释(@987654325 @) 在 std 中(主要是 perl6)(那些也在 * 等运算符上),这只是一个努力的问题(而且不应该更多)。

更一般地说:

# (%% is the "divisible by", which we can negate, becoming "!%%")
subset Even of Int where * %% 2; # * creates a closure around its expression
subset Odd of Int where -> $n { $n !%% 2 } # using a real "closure" ("pointy block")

然后你可以用智能匹配运算符~~检查一个数字是否匹配:

say 4 ~~ Even; # True
say 4 ~~ Odd; # False
say 5 ~~ Odd; # True

而且,多亏了multi subs(或多种,实际上是多种方法或其他方法),我们可以基于此进行调度:

multi say-parity(Odd $n) { say "Number $n is odd" }
multi say-parity(Even) { say "This number is even" } # we don't name the argument, we just put its type
#Also, the last semicolon in a block is optional

【讨论】:

  • 我有点畏缩的一件事是“没有什么可以阻止它们在编译时被检查”的想法。任意约束的运行时和编译时检查之间的语义和实现难度的相对差异是天文数字。
  • 阻止它们在编译时被检查的一件事是检查是不可判定的。
  • 确实,Perl 完全无法做预期的事情,其意图是从问题被标记为haskellagda 的事实推断出来的。
  • @Ven 困难不在于(仅)您的编译时检查可能涉及不纯函数,而是当它们嵌入了任意计算时,很难证明类型令人满意/等效。如果您的计算一般,我将通过“困难”将其扩展为容易无法确定。举个简单的例子,您可能想尝试对依赖于P(x * 1) == P(1 * x) 的某些类型的P(_) 进行类型检查。尽管* 的纯洁性和对x 的任何具体选择都是微不足道的......你会发现一般陈述很难证明。
  • @Ven:要检查这样的类型,编译器必须证明,对于程序的每一次可能执行,任意谓词都成立。一般来说,这个不可判定的,即使是纯函数。你可以限制一组可能的谓词——Perl 没有——但这仍然是非常困难的,不仅仅是时间问题。这是一个开放的研究问题! Liquid Types 仅管理这种检查,因为它们具有非常受约束的类型级别谓词,并使用最先进的 SMT 求解器来生成必要的证明。这不仅仅是时间问题。
【解决方案4】:

Nimrod 是一种支持这一概念的新语言。它们被称为子范围。这是一个例子。您可以在此处了解有关该语言的更多信息link

type
  TSubrange = range[0..5]

【讨论】:

  • 这个示例定义会产生静态检查还是动态检查?
  • Nimrod 可以定义float 的子集吗?
【解决方案5】:

对于第一部分,是的,那就是 Pascal,它具有整数子范围。

【讨论】:

  • 能否请您提供示例代码来说明如何完成?
  • 当然,虽然我已经几十年没有用 Pascal 编程了。类似于 VAR 年龄:0 ... 99;
  • 在编译时将数字 100 放在期望 0 到 99 范围内的某个位置是否是类型错误?如果它只是一个运行时错误,那么它并没有按照问题的要求进行。
【解决方案6】:

Whiley language 支持的内容与您所说的非常相似。例如:

type natural is (int x) where x >= 0
type probability is (real x) where 0.0 <= x && x <= 1.0

这些类型也可以像这样实现为前置/后置条件:

function abs(int x) => (int r)
ensures r >= 0:
    //
    if x >= 0:
        return x
    else:
        return -x

语言非常有表现力。使用 SMT 求解器静态验证这些不变量和前置/后置条件。这可以很好地处理上述示例,但目前在处理涉及数组和循环不变量的更复杂示例时遇到了困难。

【讨论】:

    【解决方案7】:

    对于任何感兴趣的人,我想我会在 Nim 中添加一个示例,说明截至 2019 年如何解决此问题。

    问题的第一部分是直截了当的,因为自从提出这个问题以来,Nim 已经获得了在浮点数(以及序数和枚举类型)上生成子范围类型的能力。下面的代码定义了两个新的浮点子范围类型,ProbabilityProbOne

    问题的第二部分更棘手 - 定义一个对其字段的函数有约束的类型。我提出的解决方案没有直接定义这种类型,而是使用宏 (makePmf) 将常量 Table[T,Probability] 对象的创建与创建有效 ProbOne 对象的能力联系起来(从而确保 PMF 是有效的)。 makePmf 宏在编译时进行评估,确保您无法创建无效的 PMF 表。

    请注意,我是 Nim 的新手,所以这可能不是编写此宏的最惯用方式:

    import macros, tables
    
    type
      Probability = range[0.0 .. 1.0]
      ProbOne = range[1.0..1.0]
    
    macro makePmf(name: untyped, tbl: untyped): untyped =
      ## Construct a Table[T, Probability] ensuring
      ## Sum(Probabilities) == 1.0
    
      # helper templates
      template asTable(tc: untyped): untyped =
        tc.toTable
    
      template asProb(f: float): untyped =
        Probability(f)
    
      # ensure that passed value is already is already
      # a table constructor
      tbl.expectKind nnkTableConstr
      var
        totprob: Probability = 0.0
        fval: float
        newtbl = newTree(nnkTableConstr)
    
      # create Table[T, Probability]
      for child in tbl:
        child.expectKind nnkExprColonExpr
        child[1].expectKind nnkFloatLit
        fval = floatVal(child[1])
        totprob += Probability(fval)
        newtbl.add(newColonExpr(child[0], getAst(asProb(fval))))
    
      # this serves as the check that probs sum to 1.0
      discard ProbOne(totprob)
      result = newStmtList(newConstStmt(name, getAst(asTable(newtbl))))
    
    
    makePmf(uniformpmf, {"A": 0.25, "B": 0.25, "C": 0.25, "D": 0.25})
    
    # this static block will show that the macro was evaluated at compile time
    static:
      echo uniformpmf
    
    # the following invalid PMF won't compile
    # makePmf(invalidpmf, {"A": 0.25, "B": 0.25, "C": 0.25, "D": 0.15})
    
    

    注意:使用宏的一个很酷的好处是nimsuggest(集成到 VS Code 中)甚至会突出显示创建无效 Pmf 表的尝试。

    【讨论】:

      【解决方案8】:

      Modula 3 具有子范围类型。 (序数的子范围。)因此,对于您的示例 1,如果您愿意将概率映射到某个精度的整数范围,您可以使用:

      TYPE PROBABILITY = [0..100]
      

      根据需要添加有效数字。

      参考:有关子范围序数的更多信息here

      【讨论】:

      • 在编译时将数字 200 放在期望 0 到 100 范围内的某个位置是否是类型错误?如果它只是一个运行时错误,那么它并没有按照问题的要求进行。
      • 嗨@Carl。对静态或动态类型检查的偏好是合理的,但问题并没有说明这一点。从内存中(我现在没有可用的 m3 系统),从超类(例如 INTEGER 变量)到子类(例如 [0..100] 约束变量)的分配将在运行时在 m3 中检查。但是您将200 文字分配给受约束变量的示例...理论上它可以应该在编译时进行检查。我不能肯定地说,只能肯定 Modula-3 强制执行约束类型。希望这会有所帮助。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-05-15
      • 1970-01-01
      • 2012-12-12
      • 2020-02-12
      相关资源
      最近更新 更多