纯Python中的Haskell语言特性和标准库

2020-09-22 22:38:33

Hask是一个纯Python、零依赖的库,它模仿了来自Haskell的大多数核心语言工具,包括:

完整的Hindley-Milner类型系统(带有类型类),它将对任何装饰有Hask类型签名的函数进行类型检查。

所有您喜欢的语法和控制流工具,包括运算符节、一元错误处理、保护等。

来自Haskell基库的(一些)标准库的Python移植,包括:来自Haskell基库的类型类,包括Functor、Applative、Monad、Enum、Num等。

请注意,所有这一切仍然在很大程度上是在进行alpha之前的工作,有些事情可能是错误的!

我想把尽可能多的Haskell塞到Python中,同时仍然与Python的其余部分100%兼容,只是想看看结果中是否有什么有用的想法。而且,这也很有趣!

本实验的贡献、分支和扩展总是受欢迎的!请随时提交拉取请求、打开问题或给我发电子邮件。本着这个项目的精神,鼓励尽可能多地滥用Python语言。

Hask是一个大的伪Haskell函数编程库。本自述的其余部分介绍了基础知识。

要导入所有语言功能:从Hask import*导入前奏:从Hask import Prelu导入基本库,例如数据。List:从Hask导入数据。List。

Hask提供了List类型,这是一种惰性的静态类型列表,类似于Haskell的标准列表类型。

要创建一个新列表,只需将元素放在L[和]括号内,或将现有的迭代器放在L[]内。

>;>;>;L[1,2,3]L[1,2,3]>;>;my_list=[";a";,";c";]>;>;L[my_list]L[';a';,';b';,';C';]>;>;>;L[(x**2 for x in range(1,11))]L[1...]。

要将元素添加到列表的前面,请使用cons运算符^。要组合两个列表,请使用连接运算符+。

>;>;1^L[2,3]L[1,2,3]>;>;";^(";甜蜜";^(";Prince";^L[[]]))L[";晚安";,";甜蜜";]>;>;";a";^L[1.0,10.3]#类型错误>;>;>;L[1,2]+L[3,4]L[1,2,3,4]。

列表总是延迟计算,并且只在需要时计算列表元素,因此您可以使用无限列表或在列表中放置永不结束的生成器。(当然,如果您尝试计算一个无限列表的整体,例如,通过尝试使用len查找列表的长度,您仍然可以炸毁解释器。)。

创建无限列表的一种方式是通过列表理解。与Haskell一样,列表理解有四种基本类型:

#list从1到无穷大,按1计数L[1,...]#list从1到无穷,按二L[1,3,...]#list从1到20(含),按一L[1,...,20]#list从1到20(含),按四个L[1,5,...,20]计数。

列表理解可以用于整数、长整型、浮点型、单字符串或Enum类型类的任何其他实例(稍后将详细介绍)。

Hask提供了用于列表操作(Take、Drop、Take While等)的所有Haskell函数,或者您也可以使用Python风格的索引。

>;>;>;L[1,...]。L[1...]>;>;>;来自Hask。数据。列出导入Take>;>;>;Take(5,L[";a";,";b";,...])。L[';a';,';b';,';c';,';d';,';e';]>;>;>;L[1,...][5:10]L[6,7,8,9,10]>;>;>;出自Hask。数据。列出从Hask导入的地图>;>;>;。数据。字符导入chr&>;>;>;Letters=map(chr,L[97,...])>;>;字母[:9]L[';a';,';b';,';c';,';d';,';e';,';f';,';g';,';g';,';H';,';I';]>;>;>;len(L[1,3,...])#嗯哦。

Hask允许您定义代数数据类型,这些类型是具有固定数量的类型化的未命名字段的不可变对象。

从Hask导入数据d派生自Hask导入读取、显示、公式、命令可能为Nothing,仅为=\Data。可能(";a";)==d.没有|d.仅(";a";)&;派生(Read,Show,Eq,Ord)。

要定义此类型的数据构造函数,请使用d。数据构造函数的名称排在第一位,后跟其字段。多个数据构造函数应用|分隔。如果您的数据构造函数没有字段,则可以省略括号。例如:

要自动派生类型的类型类实例,请添加&;派生(...类型类...)。在数据构造函数声明之后。目前,唯一可以派生的类型类是Eq、Show、Read、Ord和Bound。

左、右=\DATA。(";a";,";b";)==d.Left(";a";)|d.Right(";b";)&;派生(Read,Show,Eq)排序,LT,EQ,GT=\DATA。排序==d.lt|d.EQ|d.gt&;派生(Read,Show,Eq,Ord,Bound)。

现在可以使用DATA语句中定义的数据构造函数来创建这些新类型的实例。如果数据构造函数不带参数,则可以像使用变量一样使用它。

您可以使用_t查看对象的类型(相当于GHCI中的:t)。

>;>;>;来自Hask import_t>;>;>;(1)int>;>;>;_t(Just(";Soylent green";))(可能是str)>;>;>;(right((";a";,1))(a(str,int))>;>;>;_t(Just)(a->;可能a)>;>;>;_t(L[1,2,3,4])[int]。

那么这些类型的人是怎么回事呢?Hask在Python的类型系统之上运行自己的影子Hindley-Milnertype系统;_t显示特定对象的Hask类型。

在Hask中,类型化函数采用TypedFunc对象的形式,这些对象是Python函数周围的类型选择器。创建TypedFuncobject有两种方法:

使用**运算符(类似于Haskell中的::)提供类型。用于将函数或lambda转换为REPL中的TypedFunc对象,或包装已定义的Python函数。

TypedFunc对象有几个特殊属性。首先,对它们进行类型检查--当提供参数时,类型推理引擎将检查它们的类型是否与类型签名匹配,如果存在差异则引发TypeError。

>;>;>;f=(lambda x,y:x+y)**(H/int>;>;int>;>;int)>;>;>;f(2,3)5>;>;>;f(9,1.0)#类型错误。

>;>;>;g=(lambda a,b,c:a/(b+c))**(H/int>;>;int)>;>;>;g(10,2,3)2>;>;>;part_g=g(12)>;>;>;Part_g(2,2)3>;>;>;g(20,1)(4)4。

TypedFunc对象还有两个特殊的中缀操作符,即*和%操作符。*是合成运算符(相当于(.)。在Haskell中),所以f*g等同于lambda x:f(g(X))。%只是Apply操作符,它将TypedFunc应用于一个参数(相当于Haskell中的($))。该表示法(与部分应用程序结合使用时)的方便性怎么强调都不为过--这样可以消除大量嵌套括号。

>;>;>;出自Hask。前奏导入Flip>;>;>;h=(lambda x,y:x/y)**(H/Float>;>;Float>;>;Float)>;>;h(3.0)*h(6.0)*Flip(h,2.0)%36.0 9.0。

组合操作也是类型检查的,这使得它对以无指针风格编写程序很有吸引力,即,用组合将许多函数链接在一起,并依赖类型系统来捕获编程错误。

类型签名语法非常简单,它由几个基本原语组成,可以组合这些原语来构建任何类型签名:

#将两个整数加在一起@sig(H/int>;>;int>;>;int)def add(x,y):return x+y#函数参数顺序相反@sig(H/(H/";a";>;>;";b";>;";c";)>;>;";b";>;>;";a";>;>;";c";)def flip(f,b,a):return f(a,b)#在Python(非类型化)上映射Python(非类型化)函数set@sig(H/func&>>;set>;>;Set)def set_map(fn,lst):return set((fn(X)for x in lst))#将类型化函数映射到列表上@sig(H/(H/";a";>;";b";)>;>;[";a";]>;>;[";b";])def map(f,xs):返回L[(f(X)for x in xs)]#带有公式约束的类型签名@sig(H[(Eq,";a";)]/";a";>;>;[";a";]>;>;Bool)def not_in(y,xs):return not any((x==y for x in xs))#具有类型参数的类型构造函数(可能)的类型签名@sig(H/int>;>;(可能,int))def safe_div(x,y):如果y==0,则不返回任何内容,否则只返回(x/y)#不返回任何内容的函数的类型签名@sig(H/int>;>;>;无)定义发射导弹(导弹数量):打印正在发射{0}枚导弹!投掷炸弹!";%枚导弹。

还可以使用t创建类型同义词。例如,查看Rational的定义:

比率,R=\DATA。Ratio(";a";)==d.R(";a";,";a";)&;派生(Eq)Rational=t(Ratio,int)@sig(H/Rational>;>;Rational>;>;Rational)def addRational(rat1,rat2):...。

模式匹配是一种比if语句更强大的控制流工具,可用于解构可迭代和ADT,并将值绑定到局部变量。

下面的函数使用模式匹配来计算fibonaccisequence。请注意,在模式匹配表达式中,m.*用于绑定变量,p.*用于访问它们。

Def fib(X):return~(case of(X)|m(0)>;>;1|m(1)>;>;1|m(M.n)>;>;fib(P.n-1)+fib(p.n-2))>;>;fib(1)1>;>;fib(6)13。

如上面的示例所示,您可以毫不费力地组合模式匹配和递归函数。

您也可以使用^(cons运算符)解构一个迭代器。^之前的变量绑定到迭代的第一个元素,^之后的变量绑定到迭代的其余元素。下面是一个函数,它将任何可迭代的前两个元素相加,如果只有两个元素,则不返回任何内容:

@sig(H[(Num,";a";)]/[";a";]>;>;t(可能,";a";)def add_first_Two(Xs):return~(caseof(Xs)|m(M.x^(M.y^M.z))>;>;Just(P.x+P.y)|m(M.x)>;>;无)>;>;>;Add_First_Two(L[1,2,3,4,5])仅(3)>;>;>;Add_First_Two(L[9.0])无。

模式匹配对于解构ADT并将其字段分配给临时变量也非常有用。

如果您发现ADT上的模式匹配太麻烦,您也可以在ADT字段上使用数字索引。如果你把事情搞砸了,就会抛出一个索引错误(IndexError)。

类型类允许您向ADT添加附加功能。Hask实现了Haskell的所有主要类型类(完整列表请参见附录),并提供了用于创建新类型类实例的语法。

例如,让我们为“可能”类型添加一个Monad实例。但是,首先可能需要函数器和应用实例。

定义可能_FMAP(fn,x):";";";";将函数应用于(可能是a)值";";";返回~(caseof(X)|m(Nothing)>;>;Nothing|m(Just(M.x))>;>;Just(fn(p.x)实例(可能是函数器)。其中(FMAP=可能_FMAP)。

也许现在是函数器的一个实例。这允许我们调用fmap并将任何a->;b类型的函数映射到可能是a类型的值。

>;>;>;Times2=(lambda x:x*2)**(H/int>;>;int)>;>;>;toFloat=Float**(H/int>;>;Float)>;>;FMAP(toFloat,Just(10))Just(10.0)>;>;FMAP(toFloat,FMAP(Times2,Just(25)Just(50.0)。

很多对FMAP的嵌套调用很快就变得笨拙起来。幸运的是,函数器的任何实例都可以与中缀FMAP操作符*一起使用。这相当于哈斯克尔中的<;$>;。从上面重写我们的示例:

请注意,本例同时使用*作为函数组合运算符和FMAP,以将函数提升为可能值。如果这看起来令人困惑,请记住函数的FMAP只是函数组合!

既然可能是Functor的一个实例,我们可以通过定义适当的函数实现,使其成为Applicative的实例,然后成为Monad的实例。要实现应用性,我们只需要提供纯净的。要实现Monad,我们需要提供绑定。

从Hask导入应用程序,Monad实例(可能是应用程序)。其中(纯=仅)实例(可能是Monad)。其中(bind=lambda x,f:~(case of(X)|m(Just(M.a))>;>;f(p.a)|m(Nothing)>;>;Nothing))。

@sig(H/int>;>;int>;>;t(可能,int))def safe_div(x,y):如果y==0,则不返回任何内容,否则仅从Hask返回(x/y)>;>;。前奏导入翻转>;>;>;divBy=flip(Safe_Div)>;>;>;divBy(3)Just(3)>;>;>;divBy(2)>;>;divBy(3)Just(1)>;>;>;Just(12)>;divBy(2)>;>;divBy(3)Just(1)>;>;>;Just(12)>;>;divBy(0)>;>;divBy(6)无。

与在Haskell中一样,List也是一个monad,List类型的绑定只是connecatMap。

>;>;>;出自Hask。数据。LIST IMPORT REPLICATE>;>;L[1,2]>;>;Replicate(2)>;Replicate(2)L[1,1,1,1,2,2,2]。

类Person(Object):def__init__(自我,姓名,年龄):自我。名称=名称自我。Age=age实例(Eq,Person)。式中(eq=λp1,p2:p1。名称==p2。名称和p1。年龄==p2。年龄)>;>;>;人(";Philip Wadler";,59)==人(";Simon Peyton Jones";,57)false。

如果您的ADT需要Show、Eq、Read、Ord和BoundedType类的实例,建议使用派生自动生成实例,而不是手动定义它们。

定义您自己的类型类非常简单--看看Help(Typeclass),看看在Data.Functor和Data.Num中定义的类型类,看看它是如何完成的。

Hask还支持操作员部分(例如,Haskell中的(1+))。节只是TypedFunc对象,因此它们会自动进行Currate和类型检查。

>;>;>;来自Hask import__>;>;>;f=(__-20)*(2**__)*(__+3)>;>;>;f(10)8172>;>;(90/__)*(10+_))*Just(20)Just(3)>;>;>;从哈斯克寄来的。数据。列出导入Take While>;>;>;Take While(__<;5,L[1,...])。L[1,2,3,4]>;>;>;(__+__)(';Hello';,';world';)';Hello world&39;>;>;>;(_**__)(2)(10)1024>;>;来自Hask。数据。列出导入zipWith,Take>;>;>;Take(5)%zipWith(__*__,L[1,...],L[1,...])。L[1,4,9,16,25]。

如您所见,这比使用lambda并使用(lambda x:...)**(H/...)添加类型签名要容易得多。语法。

此外,SECTIONS创建的TypedFuncs的类型总是多态的,以允许任何运算符重载。

请注意,如果您使用的是IPython,则Hask__将与IPython特别的双下划线变量冲突。为避免冲突,可以在IPython中使用from Hask import__as_s。

如果您不需要模式匹配的全部功能,而只需要一条neaterswitch语句,您可以使用卫士。警卫的语法几乎与模式匹配的语法相同。

与在Haskell中一样,否则将始终求值为True,并可用作保护表达式中的acatch-all。如果没有找到匹配项(并且不存在其他子句),则将引发NoGuardMatchException。

>;>;>;来自Hask import Guard,c,否则>;>;>;粥_温度=80>;>;>;~(Guard(粥_温度)...。|c(__<;20)>;";粥太凉!";...|c(__<;90)>;>;";粥正好!";...|c(__<;150)>;";粥太烫!";...|否则>;>;";粥已经变成热核粥了!';粥恰到好处!';

如果您需要更复杂的条件,您可以随时使用lambdas、RegularPython函数,或者在您的守卫条件中使用任何其他可调用的函数。

Def EXTEMPORD_PASSWORD_SECURITY(PASSWORD):Analysis=~(Guard(Password)|c(lambda x:en(X)>;20)>;&34;哇,这是一个安全密码";|c(lambda x:en(X)<;5)&>&>您让布鲁斯·施奈尔哭了";|c(_==";12345";)&>。与我的行李相同的组合!";|否则&>&>&34;希望它';不是密码';密码';&34;)退货分析>;>;>;核启动代码=";12345";>;>;检查_密码_安全(核启动代码)';与我的行李的组合相同!';

如果您想使用Maybe and One来处理由定义在Hask外部的PythonFunctions引发的错误,您可以使用_Maybe和In_One中的修饰符来创建调用原始函数的函数,并返回包装在Maybe或任意值中的结果。

如果包装在_中的函数可能引发异常,则包装的函数将不返回任何内容。否则,结果将被封装在Just中返回。

定义吃_奶酪(奶酪):如果奶酪<;=0:提高值错误(";超出奶酪错误";)返回奶酪-1可能_吃=可能(吃_奶酪)>;>;>;可能_吃(1)只(0)>;>;>;可能_吃(0)什么都不返回。

请注意,这相当于将原始函数提升到Maybemonad中。也就是说,它的类型已经从函数更改为a->;也许是b。这使得在现有的Python函数中更容易使用Haskell中常见的方便的Monad错误处理风格。

继续这个愚蠢的例子,让我们试着吃三块奶酪,如果尝试不成功,什么也不返回:

>;>;>;奶酪=10>;>;>;奶酪_Left=Just(奶酪)>;>;可能_Eat>;>;可能_Eat>;>;>;奶酪_Left Just(7)>;>;>;奶酪=1>;>;>;奶酪_Left=Just(奶酪)>;>;可能_Eat>;>;可能_Eat>;>;可能_Eat>;>;>;奶酪_什么也没留下

请注意,我们采用了抛出异常的常规Python函数,现在正在以类型安全的单行方式处理它。

这两个函数中的任何一个函数的工作方式都与in_be类似。如果抛出异常,包装函数将返回左侧包装的异常。否则,将返回右包装的结果。

Prelude、Data.List、Data.Maybe、Data.Other、Data.Monoid等您喜欢的所有函数也都实现了。每件事都有很好的文档记录,所以如果你对某些函数或类型类不太确定,可以随意使用帮助。有关模块的完整列表,请参阅下面的附录。一些亮点:

>;>;>;出自Hask。数据。可以从Hask导入mapMaybe>;>;>;mapMaybe(safe_div(12))%L[0,1,3,0,6]L[12,4,2]>;>;。数据。List import isInfix Of>;>;>;isInfix Of(L[2,8],L[1,4,6,2,8,3,7])True>;>;>;from Hask。控制力。单项导入联接&>;&>;>;联接(Just(Just(1)Just(1)。

Hask还为__Builtins__中的所有内容提供了TypeFunc包装器,以便于兼容性。(最终,Hask将为大部分Python标准库提供类型化包装。)。

>;>;>;出自Hask。从Hask进口Flip>;>;>;前奏。数据。从Hask导入元组nd>;>;>;。蟒蛇。内置。

.