Is there a language with constrainable types?

后端 未结 8 1002
北海茫月
北海茫月 2020-12-09 15:59

Is there a typed programming language where I can constrain types like the following two examples?

  1. A Probability is a floating point number with minimum val

相关标签:
8条回答
  • 2020-12-09 16:38

    For the first part, yes, that would be Pascal, which has integer subranges.

    0 讨论(0)
  • 2020-12-09 16:38

    Modula 3 has subrange types. (Subranges of ordinals.) So for your Example 1, if you're willing to map probability to an integer range of some precision, you could use this:

    TYPE PROBABILITY = [0..100]
    

    Add significant digits as necessary.

    Ref: More about subrange ordinals here.

    0 讨论(0)
  • 2020-12-09 16:39

    Nimrod is a new language that supports this concept. They are called Subranges. Here is an example. You can learn more about the language here link

    type
      TSubrange = range[0..5]
    
    0 讨论(0)
  • 2020-12-09 16:41

    What you want is called refinement types.

    It's possible to define Probability in Agda: Prob.agda

    The probability mass function type, with sum condition is defined at line 264.

    There are languages with more direct refinement types than in Agda, for example ATS

    0 讨论(0)
  • 2020-12-09 16:43

    You can do this in Haskell with Liquid Haskell which extends Haskell with refinement types. The predicates are managed by an SMT solver at compile time which means that the proofs are fully automatic but the logic you can use is limited by what the SMT solver handles. (Happily, modern SMT solvers are reasonably versatile!)

    One problem is that I don't think Liquid Haskell currently supports floats. If it doesn't though, it should be possible to rectify because there are theories of floating point numbers for SMT solvers. You could also pretend floating point numbers were actually rational (or even use Rational in Haskell!). With this in mind, your first type could look like this:

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

    Your second type would be a bit harder to encode, especially because maps are an abstract type that's hard to reason about. If you used a list of pairs instead of a map, you could write a "measure" like this:

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

    (You might want to wrap [] in a newtype too.)

    Now you can use total in a refinement to constrain a list:

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

    The neat trick with Liquid Haskell is that all the reasoning is automated for you at compile time, in return for using a somewhat constrained logic. (Measures like total are also very constrained in how they can be written—it's a small subset of Haskell with rules like "exactly one case per constructor".) This means that refinement types in this style are less powerful but much easier to use than full-on dependent types, making them more practical.

    0 讨论(0)
  • 2020-12-09 16:46

    For anyone interested, I thought I'd add an example of how you might solve this in Nim as of 2019.

    The first part of the questions is straightfoward, since in the interval since since this question was asked, Nim has gained the ability to generate subrange types on floats (as well as ordinal and enum types). The code below defines two new float subranges types, Probability and ProbOne.

    The second part of the question is more tricky -- defining a type with constrains on a function of it's fields. My proposed solution doesn't directly define such a type but instead uses a macro (makePmf) to tie the creation of a constant Table[T,Probability] object to the ability to create a valid ProbOne object (thus ensuring that the PMF is valid). The makePmf macro is evaluated at compile time, ensuring that you can't create an invalid PMF table.

    Note that I'm a relative newcomer to Nim so this may not be the most idiomatic way to write this macro:

    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})
    
    

    Note: A cool benefit of using a macro is that nimsuggest (as integrated into VS Code) will even highlight attempts to create an invalid Pmf table.

    0 讨论(0)
提交回复
热议问题