功能思维。 第三部分

有关函数式编程的系列文章的第三部分已经提出。 今天,我们将讨论这种范例的所有类型,并展示其用法示例。 有关原始类型,广义类型的更多信息,以及更多内容!




现在我们对功能有了一些了解,我们将看到类型如何与功能(例如域和范围)交互。 本文只是一篇评论。 为了更深入地浸入类型,有一系列“了解F#类型”


首先,我们需要更好地了解类型符号。 我们看到箭头符号“ -> ”将域和范围分开。 因此,函数签名始终如下所示:


 val functionName : domain -> range 

其他一些功能示例:


 let intToString x = sprintf "x is %i" x //  int  string let stringToInt x = System.Int32.Parse(x) 

如果在交互式窗口中执行此代码,则可以看到以下签名:


 val intToString : int -> string val stringToInt : string -> int 

他们的意思是:


  • intToString具有类型为int的域,该域映射到string类型的范围。
  • stringToInt具有string类型的域,该域映射到int类型的范围。

原始类型


预期的基本类型为:字符串,整数,浮点型,布尔型,字符型,字节型,以及.NET类型系统的许多其他派生类。


带有基本类型的函数的更多示例:


 let intToFloat x = float x // "float" -  int  float let intToBool x = (x = 2) // true  x  2 let stringToString x = x + " world" 

及其签名:


 val intToFloat : int -> float val intToBool : int -> bool val stringToString : string -> string 

类型注释


在前面的示例中,F#编译器正确定义了参数和结果的类型。 但这并不总是会发生。 如果尝试执行以下代码,则会出现编译错误:


 let stringLength x = x.Length => error FS0072: Lookup on object of indeterminate type 

编译器不知道参数“ x”的类型,因此,它不知道“ Length”是否是有效方法。 在大多数情况下,可以通过将“类型注释”传递给F#编译器来解决。 然后,他将知道要使用哪种类型。 在固定版本中,我们指示类型“ x”为字符串。


 let stringLength (x:string) = x.Length 

x:string参数周围的花括号很重要。 如果跳过它们,编译器将确定该字符串为返回值! 也就是说,如下面的示例所示,使用冒号表示返回值的类型。


 let stringLengthAsInt (x:string) :int = x.Length 

我们指出参数x是一个字符串,返回值是一个整数。


函数类型作为参数


以其他函数为参数或返回一个函数的函数称为高阶函数高阶函数有时缩写为HOF)。 它们被用作抽象来尽可能地设置一般行为。 这种功能在F#中非常常见,大多数标准库都使用它们。


考虑函数evalWith5ThenAdd2 ,该函数将一个函数作为参数,然后从5计算该函数并将2加到结果中:


 let evalWith5ThenAdd2 fn = fn 5 + 2 //   ,   fn(5) + 2 

该函数的签名如下所示:


 val evalWith5ThenAdd2 : (int -> int) -> int 

您可以看到domain是(int->int) ,range是int 。 这是什么意思? 这意味着输入参数不是一个简单的值,而是从intint许多函数中的一个函数。 输出值不是函数,而只是int


让我们尝试:


 let add1 x = x + 1 //  -  (int -> int) evalWith5ThenAdd2 add1 //   

并获得:


 val add1 : int -> int val it : int = 8 

正如我们从签名中看到的,“ add1 ”是一个将int映射到int的函数。 它是evalWith5ThenAdd2的有效参数,其结果是8。


顺便说一句,特殊词“ it ”用于表示最后的计算值,在这种情况下,这是我们等待的结果。 这不是关键字,只是命名约定。


另一种情况:


 let times3 x = x * 3 // -  (int -> int) evalWith5ThenAdd2 times3 //   

给出:


 val times3 : int -> int val it : int = 17 

从签名可以看出,“ times3 ”也是一个将int映射到int的函数。 这也是evalWith5ThenAdd2的有效参数。 计算结果为17。


请注意,输入数据是类型敏感的。 如果传递的函数使用float而不是int ,则将无济于事。 例如,如果我们有:


 let times3float x = x * 3.0 // -  (float->float) evalWith5ThenAdd2 times3float 

尝试进行编译时,编译器将返回错误:


 error FS0001: Type mismatch. Expecting a int -> int but given a float -> float 

报告输入函数必须是int->int类型的函数。


用作输出


值函数也可以是函数的结果。 例如,以下函数将生成一个“加法器”函数,该函数将添加一个输入值。


 let adderGenerator numberToAdd = (+) numberToAdd 

她的签名:


 val adderGenerator : int -> (int -> int) 

表示生成器采用一个int并创建一个将ints映射到ints的函数(“加法器”)。 让我们看看它是如何工作的:


 let add1 = adderGenerator 1 let add2 = adderGenerator 2 

创建了两个加法器功能。 第一个创建一个将输入加1的函数,第二个创建一个加2的函数。请注意,签名正是我们所期望的。


 val add1 : (int -> int) val add2 : (int -> int) 

现在您可以照常使用生成的函数,它们与显式定义的函数没有什么不同:


 add1 5 // val it : int = 6 add2 5 // val it : int = 7 

使用类型注释来限制函数类型


在第一个示例中,我们看了一个函数:


 let evalWith5ThenAdd2 fn = fn 5 +2 > val evalWith5ThenAdd2 : (int -> int) -> int 

在此示例中,F#可以得出结论,“ fn ”将int转换为int ,因此其签名为int->int


但是在以下情况下“ fn”的签名是什么?


 let evalWith5 fn = fn 5 

显然,“ fn ”是一种需要int的函数,但是它返回什么呢? 编译器无法回答此问题。 在这种情况下,如果有必要指示函数的类型,则可以为函数参数以及原始类型添​​加注释类型。


 let evalWith5AsInt (fn:int->int) = fn 5 let evalWith5AsFloat (fn:int->float) = fn 5 

此外,您可以确定返回类型。


 let evalWith5AsString fn :string = fn 5 

因为 主函数返回string ,函数“ fn ”也被强制返回string 。 因此,没有必要明确指定类型“ fn ”。


输入“单位”


在编程过程中,有时我们希望函数执行某些操作而不返回任何内容。 考虑函数“ printInt ”。 该函数实际上什么也不返回。 它只是将字符串打印到控制台,这是执行的副作用。


 let printInt x = printf "x is %i" x //    

她的签名是什么?


 val printInt : int -> unit 

什么是“ unit ”?


即使该函数不返回值,它仍然需要范围。 在数学世界中没有“无效”函数。 每个函数必须返回某些内容,因为该函数是一个映射,并且该映射必须显示某些内容!



因此,在F#中,像这样的函数会返回一种特殊的结果,称为“ unit ”。 它仅包含一个值,用“ () ”表示。 您可能会认为unit()分别类似于C#中的“ void”和“ null”。 但是与它们不同的是, unit是实际类型,而()实际值。 要验证这一点,只需执行以下操作:


 let whatIsThis = () 

将收到以下签名:


 val whatIsThis : unit = () 

表示标签“ whatIsThis ”的类型为unit并与值()关联。


现在,回到“ printInt ”签名,我们可以理解该条目的含义:


 val printInt : int -> unit 

此签名表明printInt具有int的域,这意味着我们不感兴趣。


没有参数的功能


现在我们了解了unit ,是否可以预测它在不同上下文中的出现? 例如,尝试创建可重用的函数“ hello world”。 由于没有输入或输出,因此可以期望签名unit -> unit 。 让我们看看:


 let printHello = printf "hello world" //    

结果:


 hello world val printHello : unit = () 

不完全符合我们的预期。 立即输出“ Hello world”,结果不是函数,而是类型unit的简单值。 我们可以说这是一个简单的值,因为正如我们前面所看到的,它具有以下形式的签名:


 val aName: type = constant 

在此示例中,我们看到printHello一个简单的value () 。 这不是我们以后可以调用的函数。


printIntprintHello什么printHello ? 在printInt的情况下,直到我们知道参数x printInt值才能确定该值,因此定义是一个函数。 对于printHello ,没有参数,因此可以在右侧定义右侧。 并且它等于()并且具有向控制台输出的形式的副作用。


您可以创建一个没有参数的真正的可重用函数,从而强制定义具有一个unit参数:


 let printHelloFn () = printf "hello world" //    

现在她的签名等于:


 val printHelloFn : unit -> unit 

并调用它,我们必须将()作为参数传递:


 printHelloFn () 

使用忽略功能加强单位类型


在某些情况下,编译器需要unit类型并抱怨。 例如,以下两种情况都将导致编译器错误:


 do 1+1 // => FS0020: This expression should have type 'unit' let something = 2+2 // => FS0020: This expression should have type 'unit' "hello" 

为了在这些情况下提供帮助,有一个特殊的ignore函数,该函数接受任何内容并返回unit 。 该代码的正确版本可能是这样的:


 do (1+1 |> ignore) // ok let something = 2+2 |> ignore // ok "hello" 

通用类型


在大多数情况下,如果函数参数的类型可以是任何类型,则需要对它进行一些说明。 F#在这种情况下使用.NET泛型。


例如,以下函数通过添加一些文本将参数转换为字符串:


 let onAStick x = x.ToString() + " on a stick" 

无论什么类型的参数,所有对象都可以在ToString()


签名:


 val onAStick : 'a -> string 

'a是什么类型? 在F#中,这是一种指示在编译时未知的泛型类型的方法。 “ a”之前的撇号表示类型是通用的。 等效于C#中的此签名:


 string onAStick<a>(); //   string OnAStick<TObject>(); // F#-   'a    // C#'-   "TObject"   

应该理解,即使使用泛型类型,此F#函数仍具有强类型。 它接受Object类型的参数。 强类型输入是好的,因为它使您可以在编写函数时保持其类型安全。


相同的函数用于intfloatstring


 onAStick 22 onAStick 3.14159 onAStick "hello" 

如果有两个通用参数,则编译器将为它们提供两个不同的名称: 'a代表第一个, 'b代表第二个,依此类推。” 例如:


 let concatString xy = x.ToString() + y.ToString() 

此签名中将有两种通用类型: 'a'b


 val concatString : 'a -> 'b -> string 

另一方面,编译器识别何时仅需要一个通用类型。 在以下示例中, xy必须具有相同的类型:


 let isEqual xy = (x=y) 

因此,函数签名对于两个参数具有相同的通用类型:


 val isEqual : 'a -> 'a -> bool 

当涉及到列表和其他抽象结构时,通用参数也非常重要,在以下示例中我们将看到很多。


其他种类


到目前为止,仅讨论了基本类型。 这些类型可以通过各种方式组合为更复杂的类型。 它们的完整分析将在另一个系列的后面部分中,但与此同时,在这里我们将对其进行简要分析,以便您可以在函数签名中识别它们。


  • 元组 这是由其他类型组成的一对,三重等。 例如, ("hello", 1)是一个基于stringint的元组。 逗号是元组的标志;如果在F#中的某个地方看到逗号,则几乎可以保证它是元组的一部分。
    在函数签名中,元组被写为所涉及的两种类型的“产品”。 在这种情况下,元组将是以下类型:

 string * int // ("hello", 1) 

  • 集合 。 最常见的是列表(列表),序列(序列)和数组。 列表和数组的大小是固定的,而序列可能是无限的(在幕后,序列是相同的IEnumrable )。 在函数签名中,它们具有自己的关键字:“ list ”,“ seq ”和“ [] ”表示数组。

 int list // List type  [1;2;3] string list // List type  ["a";"b";"c"] seq<int> // Seq type  seq{1..10} int [] // Array type  [|1;2;3|] 

  • 选项(可选类型) 。 这是对可能丢失的对象的简单包装。 有两个选项:“ Some (当值存在时)和“ None (当值不存在时)。 在函数签名中,它们具有自己的关键字“ option ”:

 int option // Some 1 

  • 标记的协会(有区别的工会) 。 它们由其他类型的许多变体构建而成。 我们在“为什么使用F#?”中看到了一些示例。 。 在函数签名中,它们由类型名称引用;它们没有特殊的关键字。
  • 记录类型(记录) 。 类型,如数据库结构或行,一组命名值。 我们还在“为什么使用F#?”中看到了一些示例。 。 在函数签名中,它们由类型名称调用,也没有自己的关键字。

测试您对类型的理解


以下是一些表达式,用于测试您对函数签名的理解。 要进行检查,只需在交互式窗口中运行它们即可!


 let testA = float 2 let testB x = float 2 let testC x = float 2 + x let testD x = x.ToString().Length let testE (x:float) = x.ToString().Length let testF x = printfn "%s" x let testG x = printfn "%f" x let testH = 2 * 2 |> ignore let testI x = 2 * 2 |> ignore let testJ (x:int) = 2 * 2 |> ignore let testK = "hello" let testL() = "hello" let testM x = x=x let testN x = x 1 // :     x? let testO x:string = x 1 // :    :string ? 

其他资源


F#的教程很多,包括那些具有C#或Java经验的人的材料。 当您深入了解F#时,以下链接可能会很有用:



还介绍了其他几种开始学习F#的方法


最后,F#社区非常适合初学者。 在Slack上,由F#Software Foundation支持的聊天非常活跃,您可以自由加入初学者室。 我们强烈建议您这样做!


不要忘记访问俄语社区F#的网站 ! 如果您对学习语言有任何疑问,我们将很乐意在聊天室中讨论这些问题:



关于翻译作者


@kleidemos翻译
在F#开发人员俄语社区的努力下进行了翻译和编辑更改。 我们也感谢@schvepsss@shwars为本文准备发表。

Source: https://habr.com/ru/post/zh-CN422115/


All Articles