Expresso:一种具有多态可扩展行类型的简单表达式语言

2020-06-13 02:52:30

Expresso是一种最小静态类型的函数式编程语言,设计时考虑了嵌入式和/或可扩展性。这种最小语言的可能用例包括配置(如Nix)、数据交换(如JSON),甚至是自定义外部DSL的起点。

Expresso是一种函数式语言,因此我们使用lambda术语作为基本的抽象手段。要创建命名函数,我们只需使用let绑定一个lambda即可。我曾想过使用Nix风格的lambda语法,例如x:x作为标识函数,但是许多主流语言,不仅仅是Haskell,都使用箭头来表示lambda术语。箭头也与我们用来表示类型的符号一致。因此,Expresso使用箭头->;来表示lambdas,要绑定的参数在左边,表达式主体在右边,例如x->;x表示标识。

Expresso记录建立在以行扩展为基本基元的行类型之上。与以连接为基元构建的更高级系统相比,这提供了一个非常简单和易于使用的类型系统。然而,即使在这个简单的系统中,使用差异记录也可以非常容易地对串联进行编码。

当然,记录可以包含任意类型,并且可以任意嵌套。他们也可以在平等方面进行比较。点运算符(SELECT)用于投影值。

Expresso REPL键入:Help或:h以获取命令列表从/HOME/Tim/expresso/prereude.xλ>;{x=1}.x1λ>;{x={y=";foo";},z=[1,2,3]}.x.y";foo";λ>;{x=1,y=True}=={y=True,

使用选择消除记录。并使用扩展名|进行介绍。例如,记录文字:

行类型使用缺乏约束来禁止重叠字段名称。例如,以下代码类型错误:

{x=1,x=2}--不进行类型检查!让{x=";bar";|r}中的r={x=";foo";}--不进行类型检查!

通过REPL打印出推断的行类型时,会显示缺少的约束,例如:

λ>;:为所有r.(r\x)=>;{r}->;{x:int|r}键入r->;{x=1|r}。

在上面的输出中,REPL报告这个lambda可以接受底层行类型为r的记录,只要r满足它没有字段x的约束。

文字记录的类型是关闭的,因为字段集是完全已知的:

但是,通过推断打开的记录类型,我们允许具有冗余字段的记录作为函数的参数:

λ>;设sqmag={x,y}->;x*x+y*yλ>;:键入sqmagforall a r。(num a,r\x\y)=>;{x:a,y:a|r}->;a。

打开的记录类型由记录尾部的行类型指示。

请注意,上面的sqmag函数定义使用了字段双关。我们也可以写成:

在记录参数上匹配时,有时可能需要提供新名称以将字段的值绑定到,例如:

λ>;让Add={x=r,y=s}{x=u,y=v}->;{x=r+u,y=s+v}。

我们可以使用限制原语\删除字段。例如,以下内容将进行类型检查:

记录可以用作一个简单但功能强大的模块系统。例如,设想一个模块";List.x&34;具有对列表的派生操作:

设Reverse=foldl(xs x->;x::xs)[];intercalate=xs xss->;conat(点缀xs xss);.--Exportsin{Reverse,Intercalate,.}。

λ>;let List=IMPORT";List.x&34;λ>;:为所有a.[a]->;[a]->;[a]键入list.intercalateforall a.[a]->;[a]。

具有多态函数的记录可以作为lambda参数传递,并使用更高级别的多态保持多态。要实现这一点,我们必须为Expresso提供合适的参数类型注释。例如:

设f=(m:for all a.{Reverse:[a]->;[a]|_})->;{l=m.verse[True,False],r=m.verse[1,2,3]}

上面的函数f采用包含多态函数Reverse的模块";m。我们使用单个冒号:后跟我们期望的类型来注释m。注意记录尾部的下划线_。这是一个类型通配符,意味着我们已经指定了一个分部类型签名。此类型通配符允许我们使用此签名传递包含反向函数的任意模块。要查看f的完整类型签名,我们可以使用Expresso REPL:

λ>;:t fforall r.(r\Reverse)=>;(for all a.{Reverse:[a]->;[a]|r})->;{l:[bool],r:[int]}。

请注意,代表其余模块字段的r是顶级限定符。通配符类型在这里特别有用,因为它允许我们避免为整个函数创建顶级签名和显式命名此行变量。更一般地,类型通配符允许我们不指定类型签名的某些部分。

当然,函数f现在可以应用于满足类型签名的任何模块:

要对串联进行编码,我们可以使用扩展记录的函数,并使用简单的函数组合来组合它们:

设f=(r->;{x=";foo";,y=True|r})>;>;(r->;{z=";bar";|r})。

λ>;让f={|x=";foo";,y=True|}>;>;{|z=";BAR";|}λ>;f{}{z=";BAR";,x=";FOO";,y=TRUE}。

{|x=";foo";|}>;>;{|x:=";bar";|}--类型检查{|x=";foo";|}<;<;{|x:=";bar";|}--不进行类型检查!

类型{}是单位类型的一个示例。它只有一个居民,空记录{}:

记录的DUAL是变体,因为它们使用相同的底层行类型,所以它们也是多态的和可扩展的。变量是通过注入(记录选择的DUAL)引入的,例如:

λ&>;案例FOO 1,共{foo x->;x,条形{x,y}->;x+y}1。

上面的case表达式消除了闭合变量,这意味着除foo或Bar以外的任何值及其预期有效负载都将导致类型错误。为了消除开放变量,我们使用类似于扩展的语法:

λ&>;设f=x-&>;案例x,{foo x->;x,条形{x,y}->;x+y|否则->;42}λ>;f(baz{})42。

在这里,不匹配的变量被传递给lambda(以Altherly作为参数)。条形|后面的表达式通常会忽略变量或将其委托给另一个函数。

我们经常需要创建封闭的变体类型。例如,我们可能想要创建一个类似于Haskell的“可能a”的结构类型,它只有两个构造函数:Nothing和Just。这可以使用带有类型批注的智能构造函数来完成。在前言中,我们定义了等价的构造函数Just和Nothing,以及可能在此封闭集上的一个文件夹:

类型可能a=<;Just:a,Nothing:{}>;;Just:for all A.a-&>;可能a=x-&>;Just x;Nothing:Fall a.可能a=Nothing{};可能=bf m->;案例m{Just a->;f a,Nothing{}->;b}

注意,我们声明并使用类型同义词可能是a,以避免重复类型<;,只是:a,Nothing:{}>;。类型同义词可以包含在任何文件的顶部,并且具有全局作用域。

笔录限制的对偶性是变种嵌入。这允许我们通过利用非重叠字段约束来限制CASE表达式公开的行为。例如,要防止使用上述函数f的Bar替代,我们可以定义一个新函数g,如下所示:

λ>;设g=x->;f(<;|栏|>;x)λ>;:类型gforall r.(r\bar\foo)=>;foo:int|r>;->;Int;Int。

λ>;设g=x->;案例x,共{Override foo x->;x+1|f}。

λ>;设g=x->;案例x为{foo x->;x+1|<;|foo|>;>;f}λ>;:类型为gforall R1 R2。(r1\x\y,r2\Bar\foo)=>;<;foo:int,bar:{x:int,y:int|r1}|r2>;->;Int。

在内部,消除封闭变量的语法使用空变量类型<;>;,也称为void。void类型没有居民,但是我们可以使用它来定义一个荒谬的函数:

荒诞是经典逻辑中Ex FAlso Quodlibet的一个例子(任何东西都可以用矛盾作为前提来证明)。

{酒吧{}->;2|荒诞}}的{foo{}->;1|x';->;案例x';的案例x

我们可以使用Expresso作为一种轻量级数据交换格式(即带类型的JSON)。但是,我们如何根据模式验证术语呢?

简单类型批注<;Term>;:<;type>;不足以进行";架构验证";。例如,考虑以下针对允许所有内容的架构验证整数的尝试:

上面的类型检查失败,因为左侧被推断为最通用的类型(这里是一个具体的int),而右侧的类型必须较少。

这方面的一个很好的语法甜头是签名部分,尽管Expresso中的版本与Haskell提议略有不同。我们写(:t)表示id:t->;T,其中所有量词都保存在顶级。我们现在可以使用:

如果我们的模式中确实有允许任意数据的地方,我们应该使用等式约束来保证不存在部分应用的函数。例如:

(:forall a.eq a=>;{x:<;foo:int,Bar:A>;}){x=Bar id}。

λ>;(:forall a.eqa=>;{x:<;foo:int,bar:a>;}){x=bar";abc";}{x=bar";abc";}。

Expresso使用惰性计算,希望在处理大型嵌套记录时可以提高效率。

图灵等价性是通过单个FIX原语引入的,可以很容易地删除或禁用该原语。FIX对于实现开放的递归记录和动态绑定(如Nix)非常有用。

λ&>;让r=mkOverridable(Self-&>;{x=#34;foo&34;,y=self.x;;>;";bar&34;})λ>;r{Override_=;λ;Lambda&>;,x=";foo&34;,y=";foobar&34;}λ&>;覆盖r{|x:=。baz&34;,y=#34;bazbar";}。

请注意,在实践中,删除FIX和Turing等价性并不能保证终止。在没有递归或修复的情况下,仍有可能编写在宇宙的生命周期内不会终止的指数程序。

在Haskell程序中,可以将Expresso用作键入的配置文件格式。例如,让我们考虑一个备份程序的假设小配置文件:

让awsTemplate={Location=";s3://s3-eu-west-1.amazonaws.com/tim-backup";,Include=[],Exclude=[]}位于{cachePath=Default{},任务线程=覆盖2,配置文件=[{Name=";Pictures";,Source=";~/Pictures";|awsTemplate},{Name=";Music";,source=";~/Music";,排除:=[";**/*.m4a";]|awsTemplate}]}。

请注意,即使是对于这样一个小的示例,我们也已经可以利用可扩展记录的一些抽象功能来避免配置文件中的重复。

为了从Haskell程序使用此文件,我们可以定义一些相应的标称数据类型:

数据配置=Config{configCachePath::Overridable Text,configTaskThread::Overridable Integer,configProfiles::[profile]}派生Showdata Overridable a=默认|覆盖派生Showdata配置文件=profile{profileName::Text,profileLocation::Text,profileInclude::[Text],profileExclude::[Text],profileSource::Text}派生显示

使用Expresso API,我们可以编写HasValue实例来处理Haskell值的投影和注入:

导入ExpressoInstance HasValue Config,其中proj v=Config<;$>;v.:";cachePath";<;*>;v.:";taskThread";<;*>;v.:";profile";inj Config{..}=mkRecord[";cachePath";.=inj configCache.。.=inj configProfiles]Instance HasValue a=>;HasValue(Overridable A),其中proj=CHOICE[(";Override";,FMAP Override。项目),(";默认";,常量$PURE DEFAULT)]inj(Override X)=mkVariant";Override";(Inj X)inj default=mkVariant&34;Default";unitinstance HasValue Profile where proj v=profile<;$>;v.:";name";<;*>;v:";location&#。*>;v.:";exclude";<;*>;v.:";source";inj profile{..}=mkRecord[";name";.=inj profileName,";location";.=inj profileLocation,";include";.=inj profileInclude,";exclude";.=inj。

在加载配置文件之前,我们可能希望根据商定的签名(也称为。模式验证)。Expresso API提供了一个模板Haskell准引号,以便从Haskell内部方便地执行此操作:

import Expresso.TH.QQschema::Typesschema=[ExpressoType|{cachePath:<;Default:{},Override:Text>;,taskThread:<;Default:{},Override:int>;,Profiles:[{name:Text,Location:Text,Include:[Text],Exclude:[Text],Exclude:[Text],source:Text}]}|]。

因此,我们可以使用以下代码加载、验证和评估上述配置文件:

请注意,我们还可以安装我们自己的定制值/函数,以供用户在其配置文件中引用。例如:

loadConfig::FilePath->;IO(字符串配置)loadConfig=valFile';envs(仅模式),其中envs=installBinding";System";Text(inj System.Info.os)。installBinding";TakeFileName";(TFun Text Text)(Inj TakeFileName)。installBinding";TakeDirectory";(TFun Text Text)(Inj TakeDirectory)。installBinding";doPathExist";(TFun文本文本TBool)(Inj Dos PathExist)--NB:这执行IO读取$initEnvironment。

最后,我们不需要将自己限制在指定记录值的配置文件中。我们可以将Expresso函数值投影到Haskell函数(IO中),从而允许更高阶的配置文件!投影本身由HasValue类处理,就像任何其他值一样:

Haskell>;right(f::整数->;IO整数)<;-valString(仅$TFun tint tint)";x->;x+1";Haskell>;f 1 2。

可扩展记录和变体的多态类型系统";B.R.Gaster和M.P.Jones,1996。