parsix:解析不验证

2021-05-16 00:42:47

为确保输入的高级解析为右侧,满足业务逻辑所需的所有约束。

它受到亚历克西斯国王的工作的强烈启发;解析,Don' t验证"我们建议您读它即使您不熟悉Haskell。

任何非琐碎程序始终需要验证外部输入。在Kotlin Ecosystem IT' s往往是将Stream化为对象的混合,然后验证它满足更复杂的业务约束。至上的时间将以形式执行验证,如:

所有这些都共享一个关键漏洞:无法保证我们的程序实际运行此验证。

我们知道,在商业级店面恐帆必须收到一封电子邮件,但是我们的编译器是不知道的,事实上我们可以删除assertemail,它会愉快地编译成功。

而不是只是验证,而是应该将输入解析为一个形状,使得编译器意识到我们想要做的内容。在Parsix中,前一个例子将成为:

导入parsix.core.parse@jvminlineValue类电子邮件(val电子邮件:string)val parseemail:parse<字符串,电子邮件>有趣的StoreMailendPoint(InP:String){何时(val解析= ParseeMail(InP)){是OK - > storemail(parsed.value)是parseerror - > todo("处理失败")}}有趣的店铺(电子邮件:电子邮件){todo("将其存储在某个地方")}

我们更改了店铺邮件以要求电子邮件作为其参数,因此我们无法直接传递原始输入,但更重要的是,此功能不必进行任何进一步的检查以确保其输入有效,这尤为重要在大的codebase中。如果我们尝试删除Parse逻辑,编译将失败。通过对我们的电子邮件使用价值类,我们得到最好的两个世界:使用字符串运行时更具安全性和相同的性能。

通过可见性修饰符,我们可以确保必须始终通过解析构建电子邮件,以便编译器将强制我们做正确的事情。

请注意,如果我们想在解析后访问电子邮件,我们被迫评估并明确处理故障情况。

这可能看起来像更多的代码,但它很容易摘要鉴于大多数程序将有一个出口点的错误。例如,如果我们在Web服务器上工作,代码可能如下所示:

有趣< T>把手(解析:解析:paintedcase:(t) - >响应):响应=当(解析){是OK - > HappyCase(Parsed.Value)是ParseFailure - > parsefailuretoreSponse(已解析)}

这只是一个简单的抽象,具体取决于您的代码库,您可以通过例如在路由定义级别处理解析输入来更好地使其更好。

鉴于我们正在使用简单的数据类型,抽象和组合的可能性非常高。

做正确的事情:作为图书馆作者,我们努力使做法易于遵循,促进将编译器视为一个宝贵的朋友的文化而不是敌人。

可兼容:人们只能一次处理一定数量的信息,越越好。为什么为什么将问题分解为更小,可消化的解决方案,然后将所有这些都放在一起以有效地解决主要问题。

可扩展:我们希望我们的用户能够扩展我们的图书馆并以适应他们的问题的方式弯曲,而不是相反的方式。

简单:开发人员应该能够通过函数跳跃并阅读我们的代码来了解整体实现。

显式:我们更愿意制作代码明确,只隐藏什么是有意义的;如果有助于避免错误,我们并不害怕更详细的代码。

Parsix专注于构图和扩展,因此使用新的解析器即将直接地实现以下功能:

解析是一个密封的类,它模拟了我们的解析结果,并且只能有两个形状:

如果您熟悉功能编程,则此类型是专用类型的结果(也称为)。

鉴于每个企业域都与彼此不同,Parsix仅提供低级解析器和组合器,使其易于实现更复杂的Parsers。

让'我们说我们有年龄概念,我们希望确保在特定的流动中只有成年人(年龄> = 18)可以进入它:

导入parsix.core.terminalError导入parsix.core.parsed数据类年龄(val值:uint)数据类成人(val值:uint)数据类Notsultoror(Val Inp:年龄):IncerionError有趣的Parseadultage(InP:年龄):解析&lt ;成人成人> = if(inp.value> = 18)OK(成人(Inp.Value))否则NotAdulterror(Inp)

实现新的解析功能非常简单和直截了当,''这也表明您可以解析任何类型的数据,不必是一个原始类型。

值得一提的是,错误是Parsix中的第一类公民:每个解析器都应该返回特定的ParseRor类型并以允许我们为客户创建非常内信息的错误消息的方式捕获整体上下文。好的部分是它''图书馆用户,它将决定哪个是最终格式。

我们有一个特定的解析器,能够解析任何枚举,只要它实现了Parsableenum接口。

enum类attrtype(覆盖val键:字符串):parsableenum {inttype(" int"),strype(" str" str"),} val parseattributeType:parse<字符串,attrtype> =治疗蛋白< attrtype>()

有些可能建议使用enum.name而不是必须从特定界面扩展,但是,良好的做法是不将外部API绑定为enum.name .Name。显式提供密钥使代码重命名 - 安全:您可以在不无意中打破API合同的情况下自由更改枚举。

让' s在我们的域中说,我们需要使用名称,它必须是一个字符串,它的长度在3到255个字符之间:

导入parsix.core.parsed import parsix.core.ok import parsix.core.parseerror导入parsix.core.terminalError导入parsix.core.parsebetween / **使其在解析之后使用此值使用此值* / @ jvminlinevalue类名称(val原始:字符串)/ **模型我们的新错误以获得更好的错误消息* / data class nameError(Val Inp:String,override val错误:parseError):CompositeError / **制作一个新的解析,将根据业务解析长度的新解析逻辑* / val parselength:parse< int,int> = Parsebeen(3,255)有趣寄生命令(INP:String):解析<名称> =何时(解析= Parsthength(Inp.length)){是OK - >好的(名称(inp))是parseerror - > NameError(Inp,解析)}

导入parsix.core.focusedParse val arseName:parse<字符串,名称> = FocusedParse(//我们想要约束字符串长度焦点= {inp - > inp.length},// parse length parse = parsebetween(3,255),//成功值mapok = :: name,//包更好的错误消息错误maperr = :: nameError)

使用此组合器的好处是指导库用户朝向正确的路径,并确保我们增强输入和错误。

解析单个值是可以的,但我们通常需要构建需要多个元素的类型。例如:

数据类用户(val名称:名称,val年龄:年龄)数据类名称(val值:字符串)数据类年龄(Val值:uint)

假设我们将从CSV摄取数据,并且每行将是Map< String,String>喜欢:

导入parsix.core.carry import parsix.core.parsemap导入parsix.core.parseinto导入parsix.core.parsestring导入parsix.core.parseuint导入parsix.core.greedy.required val parseuser:parsemap<用户> = ParseInto(:: user.Carry()).required("名称" :: parsestring.map(:: name).required("年龄" :: parseuint.map(:: Age))

要注意的一个重要事情是:: user.carry():它将拆查用户构造函数到多个函数,每个函数都接收单个参数。移走一个参数并完全键入,编译器将抱怨如果类型抱怨不匹配;通过为每个参数具有特定类型,我们还确保我们无法交换线路。

val parseuser:parsemap<用户> = ParseInto(:: User.Carry())。重量("年龄" :: parseuint.map(:: Age))//< - 预期类型名称,有年龄。重点(& #34;姓名&#34 ;, :: parsestring.map(:: name))

请注意,我们已经包含一个用于ParseUsuser的显式类型,例如目的,编译器将推断出没有问题的类型。

与上一部分相同的例子,但这一次我们将RAW输入的DERISIALIED DERSIALIZEDIALIACIEDING

如前所述,解析器类型是多余的,所有类型必须匹配,否则将有一个编译器错误。

使用ParseInto时,我们可以在每个步骤出错。有些应用程序想收集所有这些,而其他应用则愿意立即停止。

出于这个原因,我们提供了两组不同的扩展函数,您可以在Parsix.Core.Greeey和Parsix.core.Lazy中找到它们。

贪婪的版本没有前缀,并将贪婪地收集错误,在失败后不会停止,而是尝试尽可能多的错误。如果我们有多个失败,结果将是manyErrors。

懒惰的版本以懒惰为前缀,并在失败后立即关闭短路执行。当我们需要快速反馈或跳过特别昂贵的操作时,它们更有效,应该是优选的。

懒惰和贪婪的版本都可以共同努力,但你必须小心ParseInto如何执行解析器:他们从最后一开始就运行。

c将首先运行并执行下一个解析器,即使解析失败,但是' s罚款,因为b也很快

B将始终运行,但是因为这是懒惰的,如果发生故障,它将立即停止。 这很好,因为我们的下一个解析很慢 A是最后一个解析运行,它并不重要,如果它'贪婪或懒惰 因此,如果所有解析器发生故障,我们只会收集C和B的错误。 密封类属性数据类IntAttribute(val值:int):attribute()数据类strattribute(val值:字符串):attribute() 为IntAttribute和Strattribute创建解析器应该简单,但我们如何为属性制作解析器? 通常,我们将从我们的输入中有某种类型,以指定哪些变体当前数据是指的: val attrint = mapof("类型"" int"" val"" 10")valtrtr = mapof( "类型"" str&#34 ;," val"" ten")

我们应该部分评估我们的输入并根据类型值,再次解析输入。

这是一个常见的模式,我们有ementhen(即:评估,然后继续......)运算符:

val parseattribute:parsemap<属性> = parsekey("类型",parsetype).evalthen {何时(它){attrtype。 inttype - > parsekey(" val",parseintattr)attrtype。 strype - > Parsekey(" val",parsestrattr)}}

Parsekey是一个低级运算符,创建一个Parse,它在输入映射中查找给定的键并解析.Eventhen的结果必须是另一个Parse,该解析器接收与初始解析相同的输入,因此它将是一个地图。结果是正常解析,可以像往常一样使用:

通过首先将类型转换为AttrType并使用何时使用,编译器将抱怨,以防我们添加新类型但Don' t更新此代码。始终使用枚举和密封类时使用,使您的代码易于维护。

请注意,Evalthen可以与任何解析器一起使用,它不必是枚举,并且Lambda代码可以像您需要的那样复杂。

我们重视可编译性,因此一切都可以撰写,包括我们的解析,我们有一个方便的扩展功能。

假设我们需要解析SomeNum,但是我们的初始输入是任何。在这种情况下,我们可以' t只使用parseenum,因为它需要一个字符串作为输入。值得庆幸的事我们已经有一种方法来解析任何字符串,Parsestring。让'粘在一起:

这是非常自然的,应该清楚它所做的事情:解析输入,如果成功,将结果转发到下一个解析。

此操作员是一个infix,所以如果您愿意,我们可以删除一些括号:

撰写更小的,更简单的解析器是我们首选的风格,它将增加代码可重用性并制作整体码库清洁剂。

此库及其扩展返回的所有错误必须是信息性的,并捕获创建顶部缺口错误消息所需的所有上下文。

我们很清楚,商业域彼此完全不同,即我们为什么我们决定为错误提供可扩展的层次结构,同时仍将其限制为良好的开发体验。

解析错误的底部类型是ParseError,它是密封接口,可以具有3个形状:

ImperionError是一个打开的界面,它模拟具有所需的所有信息的错误,从而从中生成错误消息,例如ResearError

CompositeError是一个打开的界面,它模拟了一种缠绕其他错误并给予更多结构的错误,如PropError。

ManyErrors是模拟错误集合的最终类,可用于贪婪解析器。

开箱即用我们只提供一个错误处理程序,能够将Parsix.core中的所有错误翻译成适合显示到英语客户的消息列表。

但是错误是可扩展的,我们鼓励图书馆用户创建特定于其业务域的新错误。此为什么用户必须提供两个扩展函数来获取错误处理程序。在最简单的形式中,我们可以为未知错误提供默认值:

导入parsix.core.makestringErrorHandler val MyHandler = MakestringErrorHandler({"未知错误,出现问题,#34;},{"未知"},)

第一个Lamba将收到我们Weren和#39;能够解析并应返回合适的错误消息。第二个lambda将收到一个rocositeError,即我们Weren'它能解析并应该返回一个合适的消息用作前缀,然后我们将递归地解析包裹的错误并构建如下消息,如:" $前缀:$留言"

导入parsix.core.requirederror导入parsix.core.terminalError导入parsix.core.compositeError数据类MyterminalError:IncermentError数据类MyCompositeError(覆盖val错误:ParseError):ComposeErrorMyHandler(MyterMinalError())== Listof("未知错误 ,出现问题")MyHandler(MyCompositeError(ResearError))== Listof("未知:所需值")