Szukaj…
Funkcje więcej niż jednego parametru
W języku F # wszystkie funkcje przyjmują dokładnie jeden parametr . Wydaje się to dziwną instrukcją, ponieważ w deklaracji funkcji można w prosty sposób zadeklarować więcej niż jeden parametr:
let add x y = x + y
Ale jeśli wpiszesz tę deklarację funkcji w interaktywnym interpretatorze F #, zobaczysz, że jej podpis typu to:
val add : x:int -> y:int -> int
Bez nazw parametrów podpis ten to int -> int -> int
. Operator ->
jest asocjacyjnie prawy, co oznacza, że ten podpis jest równoważny int -> (int -> int)
. Innymi słowy, add
jest funkcją, która przyjmuje jeden parametr int
i zwraca funkcję, która przyjmuje jeden int
i zwraca int
. Spróbuj:
let addTwo = add 2
// val addTwo : (int -> int)
let five = addTwo 3
// val five : int = 5
Możesz jednak również wywołać funkcję typu add
w bardziej „konwencjonalny” sposób, bezpośrednio przekazując jej dwa parametry, i zadziała tak, jak można się spodziewać:
let three = add 1 2
// val three : int = 3
Dotyczy to funkcji z dowolną liczbą parametrów:
let addFiveNumbers a b c d e = a + b + c + d + e
// val addFiveNumbers : a:int -> b:int -> c:int -> d:int -> e:int -> int
let addFourNumbers = addFiveNumbers 0
// val addFourNumbers : (int -> int -> int -> int -> int)
let addThreeNumbers = addFourNumbers 0
// val addThreeNumbers : (int -> int -> int -> int)
let addTwoNumbers = addThreeNumbers 0
// val addTwoNumbers : (int -> int -> int)
let six = addThreeNumbers 1 2 3 // This will calculate 0 + 0 + 1 + 2 + 3
// val six : int = 6
Ta metoda myślenia o funkcjach wieloparametrowych jako o funkcjach, które pobierają jeden parametr i zwracają nowe funkcje (które z kolei mogą przyjmować jeden parametr i zwracać nowe funkcje, dopóki nie dojdziesz do funkcji końcowej, która przyjmuje parametr końcowy i ostatecznie zwraca wynik) nazywa currying na cześć matematyka Haskell Curry, który jest znany z opracowania koncepcji. (Został wymyślony przez kogoś innego, ale Curry zasłużył sobie na większość zasług).
Ta koncepcja jest używana w całym F # i będziesz chciał się z nią zapoznać.
Podstawy funkcji
Większość funkcji w języku F # jest tworzonych ze składnią let
:
let timesTwo x = x * 2
Definiuje funkcję o nazwie timesTwo
która przyjmuje pojedynczy parametr x
. Jeśli uruchomisz interaktywną sesję F # ( fsharpi
w systemie OS X i Linux, fsi.exe
w systemie Windows) i wkleisz tę funkcję (i dodasz ;;
mówi fsharpi
aby ocenił właśnie wpisany kod), zobaczysz, że to odpowiada:
val timesTwo : x:int -> int
Oznacza to, że timesTwo
jest funkcją, która pobiera pojedynczy parametr x
typu int
i zwraca int
. Podpisy funkcji są często zapisywane bez nazw parametrów, więc często zobaczysz ten typ funkcji zapisany jako int -> int
.
Ale poczekaj! Skąd F # wiedział, że x
jest parametrem liczby całkowitej, skoro nigdy nie podałeś jego typu? Wynika to z wnioskowania typu . Ponieważ w treści funkcji pomnożono x
przez 2
, typy x
i 2
muszą być takie same. (Zasadniczo F # nie będzie domyślnie rzutował wartości na różne typy; musisz jawnie określić dowolne typecasty).
Jeśli chcesz utworzyć funkcję, która nie przyjmuje żadnych parametrów, jest to niewłaściwy sposób:
let hello = // This is a value, not a function
printfn "Hello world"
Właściwy sposób to zrobić:
let hello () =
printfn "Hello world"
Ta funkcja hello
ma unit -> unit
typu unit -> unit
, co wyjaśniono w rozdziale „Jednostka” .
Funkcje Curried kontra Tupled
Istnieją dwa sposoby definiowania funkcji z wieloma parametrami w F #, funkcjach Curled i Tupled.
let curriedAdd x y = x + y // Signature: x:int -> y:int -> int
let tupledAdd (x, y) = x + y // Signature: x:int * y:int -> int
Wszystkie funkcje zdefiniowane spoza F # (takie jak .NET Framework) są używane w F # z formularzem Tupled. Większość funkcji w podstawowych modułach F # jest używana z formą Curry.
Forma curry jest uważana za idiomatyczną F #, ponieważ pozwala na częściowe zastosowanie. Żaden z dwóch poniższych przykładów nie jest możliwy w przypadku formy Tupled.
let addTen = curriedAdd 10 // Signature: int -> int
// Or with the Piping operator
3 |> curriedAdd 7 // Evaluates to 10
Powodem tego jest to, że funkcja Curried, gdy jest wywoływana z jednym parametrem, zwraca funkcję. Witamy w programowaniu funkcjonalnym !!
let explicitCurriedAdd x = (fun y -> x + y) // Signature: x:int -> y:int -> int
let veryExplicitCurriedAdd = (fun x -> (fun y -> x + y)) // Same signature
Widać, że to dokładnie ten sam podpis.
Jednak w przypadku łączenia się z innym kodem .NET, jak w przypadku pisania bibliotek, ważne jest, aby zdefiniować funkcje za pomocą formularza Tupled.
Inlining
Inlining pozwala zamienić wywołanie funkcji na jej treść.
Jest to czasem przydatne ze względu na wydajność w krytycznej części kodu. Ale odpowiednikiem jest to, że twój zespół zajmie dużo miejsca, ponieważ treść funkcji jest duplikowana wszędzie tam, gdzie wystąpiło wywołanie. Musisz być ostrożny przy podejmowaniu decyzji, czy wstawić funkcję, czy nie.
Funkcję można wstawić za pomocą słowa kluczowego inline
:
// inline indicate that we want to replace each call to sayHello with the body
// of the function
let inline sayHello s1 =
sprintf "Hello %s" s1
// Call to sayHello will be replaced with the body of the function
let s = sayHello "Foo"
// After inlining ->
// let s = sprintf "Hello %s" "Foo"
printfn "%s" s
// Output
// "Hello Foo"
Kolejny przykład z wartością lokalną:
let inline addAndMulti num1 num2 =
let add = num1 + num2
add * 2
let i = addAndMulti 2 2
// After inlining ->
// let add = 2 + 2
// let i = add * 2
printfn "%i" i
// Output
// 8
Rura do przodu i do tyłu
Operatorzy rur służą do przekazywania parametrów do funkcji w prosty i elegancki sposób. Pozwala wyeliminować wartości pośrednie i ułatwić odczyt funkcji.
W języku F # istnieją dwa operatory potokowe:
Do przodu (
|>
): Przekazywanie parametrów od lewej do prawejlet print message = printf "%s" message // "Hello World" will be passed as a parameter to the print function "Hello World" |> print
Wstecz (
<|
): Przekazywanie parametrów od prawej do lewejlet print message = printf "%s" message // "Hello World" will be passed as a parameter to the print function print <| "Hello World"
Oto przykład bez operatorów rur:
// We want to create a sequence from 0 to 10 then:
// 1 Keep only even numbers
// 2 Multiply them by 2
// 3 Print the number
let mySeq = seq { 0..10 }
let filteredSeq = Seq.filter (fun c -> (c % 2) = 0) mySeq
let mappedSeq = Seq.map ((*) 2) filteredSeq
let finalSeq = Seq.map (sprintf "The value is %i.") mappedSeq
printfn "%A" finalSeq
Możemy skrócić poprzedni przykład i uczynić go czystszym za pomocą operatora rury do przodu:
// Using forward pipe, we can eliminate intermediate let binding
let finalSeq =
seq { 0..10 }
|> Seq.filter (fun c -> (c % 2) = 0)
|> Seq.map ((*) 2)
|> Seq.map (sprintf "The value is %i.")
printfn "%A" finalSeq
Każdy wynik funkcji jest przekazywany jako parametr do następnej funkcji.
Jeśli chcesz przekazać wiele parametrów do operatora potoku, musisz dodać |
dla każdego dodatkowego parametru i utwórz krotkę z parametrami. Natywny operator potoku F # obsługuje do trzech parametrów (|||> lub <|||).
let printPerson name age =
printf "My name is %s, I'm %i years old" name age
("Foo", 20) ||> printPerson