我最近一直在研究锡兰已经非常强大的文字系统的一些实验性新功能。 我在这篇文章中要解释的内容在技术上称为:
- 高阶泛型类型 (或类型构造函数多态或更高种类 ),以及
- 较高级别的通用类型 (或N级多态性 )。
请不要担心这种行话沙拉。 (并且,请不要尝试搜索任何一个术语,因为您会发现这些解释只会使这些非常简单的概念变得令人困惑。)坚持我,我将尽我所能以直观的方式解释这些概念。 ,而无需上述任何术语。
但首先,让我们从说明一个激励问题的两个示例开始。
此函数仅返回其参数:
Object pipeObject(Object something) => something;
此函数添加Float
:
Float addFloats(Float x, Float y) => x+y;
现代编程语言使我们可以将这些函数中的任何一个视为值,并将其传递给系统。 例如,我可以写:
Object(Object) pipeObjectFun = pipeObject;
要么:
Float(Float,Float) addFloatsFun = addFloats;
其中Object(Object)
和Float(Float,Float)
表示函数的类型 ,而pipeObject
和addFloats
是对函数的引用 。 到目前为止,一切都很好。
但是有时候,使用泛型从抽象的数据类型中抽象出来的功能很有用。 我们引入一个类型变量,以表示我们正在处理的“未知”类型的事物:
Any pipe<Any>(Any anything) => anything;
和:
Number add<Number>(Number x, Number y)
given Number satisfies Summable<Number>
=> x+y;
有时,如add()
,未知类型在某种程度上受到限制。 我们使用类型约束来表达这一点:
given Number satisfies Summable<Number>
这是锡兰(Ceylon)表示Number
只能是上限Summable<Number>
的子类型的类型的方法,即它是我们可以对其应用加法运算符+
。
现在,如果我想传递对此功能的引用该怎么办。 好吧,我通常可以做的一件事就是将未知类型确定为一个具体值:
Object(Object) pipeObjectFun = pipe<Object>;
要么:
Float(Float,Float) addFloatsFun = add<Float>;
但这有点令人失望-我们已经失去了add()
是泛型的事实。 现在,在面向对象的语言中,可以将泛型函数定义为类的成员,并在系统中传递类的实例。 这称为策略模式 。 但是不得不编写整个类来封装函数引用是不方便的。
能够写会更好:
TypeOfPipe pipeFun = pipe;
和:
TypeOfAdd addFun = add;
其中TypeOfPipe
和TypeOfAdd
是通用函数的类型。 问题在于大多数语言的类型系统中无法表示这些类型。 让我们看看如何在锡兰1.2中做到这一点。
类型功能介绍
我答应避免行话,也要避免行话。 我们唯一需要的术语就是类型函数的概念。 顾名思义,类型函数是一个接受零个或多个类型并产生一个类型的函数。 类型函数起初可能看起来很奇特和抽象,但是有一点可以帮助您理解它们:
您已经知道几乎所有关于类型函数所需的知识,因为几乎所有关于普通(值)函数的知识都适用于类型函数。
如果你留在这个比喻普通功能接地,你不会有任何问题,这篇文章的其余部分,我保证 。
因此,我们都知道普通函数是什么样的:
function addFloats(Float x, Float y) => x+y;
让我们分解一下,我们有:
- 粗箭头左侧的函数名称和参数列表,以及
- 粗箭头右侧的表达式。
类型函数看起来并没有太大不同。 它在粗箭头的左侧具有名称和(类型)参数,在右侧具有(类型)表达式。 看起来像这样:
alias Pair<Value> => [Value,Value];
啊哈! 这是我们已经看到的东西! 因此,类型函数无非就是泛型类型别名! 这个特定的类型函数接受一个类型,并生成一个元组类型,即一对,其元素属于给定类型。
实际上,并非每个类型函数都是类型别名。 通用类或接口是类型函数。 例如:
interface List<Element> { ... }
该接口声明接受类型Element
,并产生类型List<Element>
,因此它是一个类型函数。
我可以通过提供值作为参数来调用函数:
pipe("hello")
add(1.0, 2.0)
这些表达式产生值"hello"
和3.0
。
我可以通过提供类型作为参数来应用类型函数:
Pair<Float>
通过将类型函数Pair
应用于类型参数Float
[Float,Float]
此类型表达式生成类型 [Float,Float]
。
同样,我可以应用类型函数List
:
List<String>
通过将类型函数List
应用于类型实参String
,此类型表达式实际上产生了类型List<String>
。
另一方面,我可以通过只写值函数名称而不使用任何参数(例如pipe
或add
来引用值函数。 我可以对类型函数做同样的事情,编写Pair
或List
。
回到普通值的quotidian世界中,我可以写下一个匿名函数 :
(Float x, Float y) => x+y
在柏拉图式的世界中,我也可以这样做:
<Value> => [Value,Value]
最后,普通值函数可以使用Float x
这样的类型注释来约束其参数。 一个类型函数可以做同样的事情,尽管语法比较麻烦:
interface List<Element>
given Element satisfies Object
{
//define the type list
...
}
甚至匿名类型的函数也可能具有约束:
<Value> given Value satisfies Object
=> [Value,Value]
术语表:包括我在内的大多数人都使用术语“ 类型构造函数”代替“类型函数”。
类型函数是类型
现在,我们将进行关键的概念性飞跃。 回想一下,在现代语言中,功能被视为值。 我可以
- 接受一个函数,并将其分配给一个变量,然后
- 在函数体内调用该变量。
例如:
void secondOrder(Float(Float,Float) fun) {
print(fun(1.0, 2.0));
}
//apply to a function reference
secondOrder(addFloats);
//apply to an anonymous function
secondOrder((Float x, Float y) => x+y);
我们称接受接受高阶函数的函数 。
同样,我们将声明类型函数为types 。 也就是说,我可以:
- 使用类型函数并将其分配给类型变量,然后
- 在类型化的声明主体中应用该类型变量。
例如:
interface SecondOrder<Box> given Box<Value> {
shared formal Box<Float> createBox(Float float);
}
//apply to a generic type alias
SecondOrder<Pair> something;
//apply to a generic interface
SecondOrder<List> somethingElse;
//apply to an anonymous type function
SecondOrder<<Value> => [Value,Value]> somethingScaryLookin;
given Box<Value>
的类型约束指示类型变量Box
接受带有一个type参数的类型函数。
现在,这里要注意一件事。 在这一点上,类型函数是类型的概念仅是形式上的声明。 定义我可以写下哪些类型的类型并期望我的编程语言的类型检查器能够进行推理的公理。 我还没有说过这些类型的任何实际值 !
术语表:将类型函数视为类型的能力称为高阶泛型 。
泛型函数的类型是类型函数
让我们回到激励性的例子:
Any pipe<Any>(Any anything) => anything;
Number add<Number>(Number x, Number y)
given Number satisfies Summable<Number>
=> x+y;
如果斜视,您会看到它们实际上是带有两个参数列表的函数。 第一个参数列表是:
<Any>
和:
<Number> given Number satisfies Summable<Number>
两者都接受类型。 第二个参数列表是:
(Any anything)
和:
(Number x, Number y)
因此,我们可以将每个泛型函数视为接受类型并产生普通值函数的函数。 结果函数的类型分别为Any(Any)
和Value(Value,Value)
。
因此,我们可以像这样写下第一个通用函数pipe()
的类型:
<Any> => Any(Any)
并且add()
的类型是:
<Number> given Number satisfies Summable<Number>
=> Number(Number,Number)
ew 看起来有点吓人。 但主要是由于类型约束。 由于此类通用函数类型非常冗长,因此我们可以为其分配别名:
alias AdditionLikeOperation
=> <Number> given Number satisfies Summable<Number>
=> Number(Number,Number);
或者,等效地,但更简单地:
alias AdditionLikeOperation<Number>
given Number satisfies Summable<Number>
=> Number(Number,Number);
那是最困难的部分,我们快完成了。
泛型功能参考
现在,我们可以将这些类型用作对通用函数的引用类型:
<Any> => Any(Any) pipeFun = pipe;
AdditionLikeOperation addFun = add;
我们可以通过提供类型参数来应用这些函数引用:
String(String) pipeString = pipeFun<String>;
Object(Object) pipeObject = pipeFun<Object>;
Float(Float,Float) addFloats = addFun<Float>;
Integer(Integer,Integer) addInts = addFun<Integer>;
或者,或者,我们可以立即将泛型函数引用应用于值参数,并让Ceylon推断类型参数,就像直接调用函数时通常这样做的那样:
String hi = pipeFun("hello");
Integer zero = pipeFun(0);
Float three = addFun(1.0, 2.0);
String helloWorld = addFun("Hello", "World");
现在我们已经解决了开始时提出的问题!
现在开始讨论:类型<Any> => Any(Any)
和AdditionLikeOperation
都是类型函数。 实际上, 任何通用函数的类型都是这种通用形式的类型函数:
<TypeParameters> => ReturnType(ParameterTypes)
同样,这种通用形式的每个类型函数都是某些通用函数的类型。
因此,我们现在证明了某些类型函数不仅是类型,而且是值的类型,这些值是对诸如pipe
和add
类的通用函数的引用。
术语表:将通用函数视为值的能力称为高级通用 。
泛型函数的抽象
最后,我们可以将所有这些用于有用的事情。 让我们考虑一个从所有对象中抽象出来的扫描器库:
- 字符类型,
- 令牌类型,
- 令牌所在的容器的种类。
然后,我们可能具有带有这种签名的scan()
函数:
"Tokenize a stream of characters, producing
a stream of tokens."
Stream<Token> scan<Char,Token,Stream>
(grammar, characterStream, newToken, newStream)
//Note: Stream is a reference to a type function!
given Stream<Element> satisfies {Element*} {
//parameters:
"The token grammar."
Grammar grammar;
"The character stream to tokenize."
Stream<Char> characterStream;
"Constructor for tokens, accepting a
substream of characters."
Token newToken(Stream<Char> chars);
"Generic function to construct a stream
of characters or tokens."
//Note: newStream is a reference to a generic function!
Stream<Elem> newStream<Elem>({Elem*} elements);
//implementation:
Stream<Token> tokenStream;
//do all the hard work
...
return tokenStream;
}
这里:
-
Char
是未知字符类型, -
Token
是未知令牌类型, -
Stream
是一种类型函数,代表未知的容器类型,可以包含字符或标记, -
newToken
是一个函数,它接受字符的子流并创建Token
, -
characterStream
是characterStream
流,最重要的是, -
newStream
是一个通用函数,可构造任何元素类型的流,在内部用于创建字符和标记流。
我们可以像这样使用此功能:
//Let's use String as the token type, Character as the
//character type, and Iterable as the stream type.
//input a stream of characters
{Character*} input = ... ;
//and some token grammar
Grammar grammar = ... ;
//get back a stream of Strings
{String*} tokens =
scan<Character, String, Iterable>
(grammar, input, String,
//Note: a generic anonymous function!
<Elem>({Elem*} elems) => elems);
或像这样:
//use LinkedList as the stream type
import ceylon.collection { LinkedList }
//we don't need Unicode support, so let's use
//Ceylon's 8-bit Byte as our character type
alias Char => Byte;
//define our own token type
class BasicToken(LinkedList<Char> charList) {
string => String { for (b in charList)
b.unsigned.character };
}
//input a linked list of characters
LinkedList<Char> input = ... ;
//and some token grammar
Grammar grammar = ... ;
//get back a linked list of BasicTokens
LinkedList<BasicToken> tokens =
scan<Char, BasicToken, LinkedList>
(grammar, input, BasicToken,
//Note: a generic function ref!
LinkedList);
如您所见,我们的解析算法现在几乎完全从我们要使用的具体类型中抽象出来了!
编译这段代码
在Ceylon 1.2中,使用类型函数进行编程是一项实验性功能,只能与JavaScript后端结合使用。 因此,您可以在JavaScript虚拟机上运行使用类型函数的代码,而不是在JVM上运行。 我们邀请您一起玩,看看整个社区是否都认为它有用。 但是现在,语言规范中并未涉及它,并且Java后端也不支持它。
类型检查器本身支持有关更高阶和更高等级类型的极其复杂的推理。 类型函数与Ceylon强大的子类型多态系统完全集成在一起,包括联合和相交类型,以及类型推断和类型实参推断。 甚至对类型函数推断的支持也很有限! 而且这里没有任意的上限; 不仅支持等级2,还支持任意等级类型。
如果有足够的兴趣,我将在以后的文章中介绍该材料。
翻译自: https://www.javacodegeeks.com/2015/06/programming-with-type-functions-in-ceylon.html