Несколько Выходов Из Функции F#
я мог бы сделать это легко в C++ (примечание :Я не проверял это на правильность-это только для иллюстрации того, что я пытаюсь сделать):
const int BadParam = -1;
const int Success = 0;
int MyFunc(int param)
{
if(param < 0)
{
return BadParam;
}
//normal processing
return Success;
}
но я не могу понять, как выйти из рутины в начале F#. То, что я хочу сделать, это выйти из функции на плохом входе, но продолжить, если вход в порядке. Я упускаю какое-то фундаментальное свойство F# или я подхожу к проблеме неправильно, так как я только изучаю FP? Это произойдет сбой мой единственный вариант здесь?
это то, что у меня есть до сих пор, и он компилируется ok:
#light
module test1
(* Define how many arguments we're expecting *)
let maxArgs = 2;;
(* The indices of the various arguments on the command line *)
type ProgArguments =
| SearchString = 0
| FileSpec = 1;;
(* Various errorlevels which the app can return and what they indicate *)
type ProgReturn =
| Success = 0
| WrongNumberOfArgumentsPassed = 1;;
[<EntryPoint>]
let main (args:string[]) =
printfn "args.Length is %d" args.Length
let ProgExitCode = if args.Length <> maxArgs then
printfn "Two arguments must be passed"
int ProgReturn.WrongNumberOfArgumentsPassed
(* Want to exit "main" here but how? *)
else
int ProgReturn.Success
let searchstring, filespec = args.[int ProgArguments.SearchString],args.[int ProgArguments.FileSpec];
printfn "searchstring is %s" searchstring
printfn "filespec is %s" filespec
ProgExitCode;;
есть ли способ FP справиться с такого рода вещами?
5 ответов
на мой взгляд, выражения соответствия являются аналогом F# раннего выхода для вызова ошибочных условий и обработки их отдельно. Для вашего примера я бы написал:
[<EntryPoint>]
let main (args:string[]) =
printfn "args.Length is %d" args.Length
match args with
| [| searchstring; filespace |] ->
// much code here ...
int Success
| _ -> printfn "Two arguments must be passed"
int WrongNumberOfArgumentsPassed
это хорошо отделяет случай ошибки. В общем, если вам нужно выйти из середины чего-то, разделите функции, а затем поместите случай ошибки в match
. На самом деле нет предела тому, насколько маленькие функции должны быть в функциональном языке.
кроме того, использование дискриминации объединения как наборы целочисленных констант немного странные. Если вам нравится эта идиома, имейте в виду, что вам не нужно включать имя типа при обращении к ним.
в F# все состоит из выражений (в то время как во многих других языках ключевым строительным блоком является оператор). Нет способа выйти из функции рано, но часто это не требуется. В C, у вас есть if/else
блоки, где ветви состоят из операторов. В F# есть if/else
выражение, где каждая ветвь оценивает значение некоторого типа и значение всего if/else
expression-это значение одной ветви или другой.
так это C++:
int func(int param) {
if (param<0)
return BadParam;
return Success;
}
выглядит так В F#:
let func param =
if (param<0) then
BadParam
else
Success
ваш код находится на правильном пути, но вы можете переделать его, положив большую часть твоей логике в else
ветка, с логикой "раннего возврата" в if
филиала.
прежде всего, как уже отмечали другие, это не "Путь F#" (ну, не путь FP, на самом деле). Поскольку вы имеете дело не с утверждениями, а только с выражениями, на самом деле нет ничего, из чего можно вырваться. В общем случае это обрабатывается вложенной цепочкой if
..then
..else
заявления.
тем не менее, я, конечно, вижу, где есть достаточно потенциальных точек выхода, что долго if
..then
..else
цепь может быть не очень читаемой-особенно при работе с некоторым внешним API, который написан, чтобы возвращать коды ошибок, а не создавать исключения при сбоях (например, Win32 API или какой-то COM-компонент), поэтому вам действительно нужен этот код обработки ошибок. Если это так, то, похоже, способ сделать это в F#, в частности, написать процесс для него.
Вот мой первый взгляд на это:
type BlockFlow<'a> =
| Return of 'a
| Continue
type Block() =
member this.Zero() = Continue
member this.Return(x) = Return x
member this.Delay(f) = f
member this.Run(f) =
match f() with
| Return x -> x
| Continue -> failwith "No value returned from block"
member this.Combine(st, f) =
match st with
| Return x -> st
| Continue -> f()
member this.While(cf, df) =
if cf() then
match df() with
| Return x -> Return x
| Continue -> this.While(cf, df)
else
Continue
member this.For(xs : seq<_>, f) =
use en = xs.GetEnumerator()
let rec loop () =
if en.MoveNext() then
match f(en.Current) with
| Return x -> Return x
| Continue -> loop ()
else
Continue
loop ()
member this.Using(x, f) = use x' = x in f(x')
let block = Block()
пример использования:
open System
open System.IO
let n =
block {
printfn "Type 'foo' to terminate with 123"
let s1 = Console.ReadLine()
if s1 = "foo" then return 123
printfn "Type 'bar' to terminate with 456"
let s2 = Console.ReadLine()
if s2 = "bar" then return 456
printfn "Copying input, type 'end' to stop, or a number to terminate with that number"
let s = ref ""
while (!s <> "end") do
s := Console.ReadLine()
let (parsed, n) = Int32.TryParse(!s)
if parsed then
printfn "Dumping numbers from 1 to %d to output.txt" n
use f = File.CreateText("output.txt") in
for i = 1 to n do
f.WriteLine(i)
return n
printfn "%s" s
}
printfn "Terminated with: %d" n
как вы можете видеть, он эффективно определяет все конструкции таким образом, что, как только return
is обнаруженный, остальная часть блока даже не оценивается. Если блок течет "с конца" без return
, вы получите исключение во время выполнения (я не вижу никакого способа осуществить это во время компиляции до сих пор).
это поставляется с некоторыми ограничениями. Прежде всего, рабочий процесс действительно не завершен - он позволяет использовать let
, use
, if
, while
и for
внутри, но не try
..with
или try
..finally
. Это можно сделать - нужно реализовать Block.TryWith
и Block.TryFinally
- но я не могу найти документы для них до сих пор, поэтому для этого потребуется немного угадать и больше времени. Я могу вернуться к этому позже, когда у меня будет больше времени, и добавить их.
во-вторых, поскольку рабочие процессы - это просто синтаксический сахар для цепочки вызовов функций и лямбд - и, в частности, весь ваш код находится в лямбдах-вы не можете использовать let mutable
внутри рабочего процесса. Вот почему я использовал ref
и !
в приведенном выше примере кода, который является универсальным обход.
наконец, есть неизбежный штраф за производительность из-за всех вызовов лямбда. Предположительно, F# лучше оптимизирует такие вещи, чем, скажем, C# (который просто оставляет все, как есть в IL), и может встроить материал на уровне IL и делать другие трюки; но я мало знаю об этом, поэтому точная производительность, если она есть, может быть определена только профилированием.
опция, похожая на Павла, но без необходимости вашего собственного workflow builder, просто поместить блок кода в seq
выражение, и она yield
сообщения об ошибках. Затем сразу после выражения вы просто вызываете FirstOrDefault
чтобы получить первое сообщение об ошибке (или null).
поскольку выражение последовательности оценивает лениво, это означает, что оно будет продолжаться только до точки первой ошибки (предполагая, что вы никогда не вызываете ничего, кроме FirstOrDefault
на последовательность). А если нет ошибка тогда просто проходит до конца. Поэтому, если вы сделаете это таким образом, Вы сможете думать о yield
так же, как раннее возвращение.
let x = 3.
let y = 0.
let errs = seq {
if x = 0. then yield "X is Zero"
printfn "inv x=%f" (1./x)
if y = 0. then yield "Y is Zero"
printfn "inv y=%f" (1./y)
let diff = x - y
if diff = 0. then yield "Y equals X"
printfn "inv diff=%f" (1./diff)
}
let firstErr = System.Linq.Enumerable.FirstOrDefault errs
if firstErr = null then
printfn "All Checks Passed"
else
printfn "Error %s" firstErr
эта рекурсивная функция Фибоначчи имеет две точки выхода:
let rec fib n =
if n < 2 then 1 else fib (n-2) + fib(n-1);;
^ ^