Во-первых, ваша функция на самом деле не является функцией. Это ценность. Различие между функциями и значениями синтаксическое: если у вас есть какие-либо параметры, вы — функция; иначе - ты ценность. Следствие этого различия очень важно при наличии побочных эффектов: значения вычисляются только один раз, во время инициализации, и затем никогда не меняются, в то время как функции выполняются каждый раз, когда вы их вызываете.
Для вашего конкретного примера это означает, что следующая программа:
let main _ =
readMyType
readMyType
readMyType
0
запросит у пользователя только один ввод, а не три. Поскольку readMyType
является значением, оно инициализируется один раз, при запуске программы, и любая последующая ссылка на него просто получает предварительно вычисленное значение, но не выполняет код снова.
Во-вторых, да, вы правы: чтобы протестировать эту функцию, вам нужно внедрить функцию input
в качестве параметра:
let readMyType (input: unit -> string) =
input().Split(';')
|> fun x -> {Name=x.[1]; Coordinate = {
Longitude = float(x.[4].Replace(",","."))
Latitude =float(x.[5].Replace(",","."))
}}
а затем попросите тесты предоставить разные входные данные и проверить разные результаты:
let [<Test>] ``Successfully parses correctly formatted string``() =
let input() = "foo;the_name;bar;baz;1,23;4,56"
let result = readMyType input
result |> should equal { Name = "the_name"; Coordinate = { Longitude = 1.23; Latitude = 4.56 } }
let [<Test>] ``Fails when the string does not have enough parts``() =
let input() = "foo"
(fun () -> readMyType input) |> shouldFail
// etc.
Поместите эти тесты в отдельный проект, добавьте ссылку на свой основной проект, а затем добавьте средство запуска тестов в свой скрипт сборки.
ОБНОВЛЕНИЕ
Из ваших комментариев у меня сложилось впечатление, что вы стремились не только протестировать функцию как она есть (что следует из вашего первоначального вопроса), но и попросить совета по улучшению функции себя, чтобы сделать его более безопасным и удобным.
Да, определенно лучше проверять условия ошибки внутри функции и возвращать соответствующий результат. Однако, в отличие от C#, обычно лучше избегать исключений как механизма управления потоком. Исключения предназначены для исключительных ситуаций. Для таких ситуаций, которых вы никогда не ожидали. Именно поэтому они являются исключениями. Но поскольку весь смысл вашей функции заключается в анализе ввода, само собой разумеется, что недопустимый ввод является одним из нормальных условий для него.
В F# вместо создания исключений вы обычно возвращаете результат, указывающий, была ли операция выполнена успешно. Для вашей функции подходит следующий тип:
type ErrorMessage = string
type ParseResult = Success of MyType | Error of ErrorMessage
И затем соответствующим образом измените функцию:
let parseMyType (input: string) =
let parts = input.Split [|';'|]
if parts.Length < 6
then
Error "Not enough parts"
else
Success
{ Name = parts.[0]
Coordinate = { Longitude = float(parts.[4].Replace(',','.')
Latitude = float(parts.[5].Replace(',','.') }
}
Эта функция вернет нам либо MyType
, завернутый в Success
, либо сообщение об ошибке, завернутое в Error
, и мы можем проверить это в тестах:
let [<Test>] ``Successfully parses correctly formatted string``() =
let input() = "foo;the_name;bar;baz;1,23;4,56"
let result = readMyType input
result |> should equal (Success { Name = "the_name"; Coordinate = { Longitude = 1.23; Latitude = 4.56 } })
let [<Test>] ``Fails when the string does not have enough parts``() =
let input() = "foo"
let result = readMyType input
result |> should equal (Error "Not enough parts)
Обратите внимание, что, несмотря на то, что код теперь проверяет наличие достаточного количества частей в строке, возможны другие условия ошибки: например, parts.[4]
может быть недопустимым числом.
Я не собираюсь распространяться об этом дальше, так как это сделает ответ слишком длинным. Я остановлюсь только на двух моментах:
- В отличие от C#, проверка всех условий ошибки не должна заканчиваться пирамида судьбы. Валидации можно красиво комбинировать линейным образом (см. пример ниже).
- Стандартная библиотека F# 4.1 уже предоставляет тип, аналогичный приведенному выше
ParseResult
, с именем Result<'t, 'e>
.
Подробнее об этом подходе читайте в этой замечательной публикации (и не забудьте изучить все ссылки оттуда, особенно видео).
И здесь я оставлю вас с примером того, как может выглядеть ваша функция с полной проверкой всего (но имейте в виду, что это все еще не самая чистая версия):
let parseFloat (s: string) =
match System.Double.TryParse (s.Replace(',','.')) with
| true, x -> Ok x
| false, _ -> Error ("Not a number: " + s)
let split n (s:string) =
let parts = s.Split [|';'|]
if parts.Length < n then Error "Not enough parts"
else Ok parts
let parseMyType input =
input |> split 6 |> Result.bind (fun parts ->
parseFloat parts.[4] |> Result.bind (fun lgt ->
parseFloat parts.[5] |> Result.bind (fun lat ->
Ok { Name = parts.[1]; Coordinate = { Longitude = lgt; Latitude = lat } } )))
Использование:
> parseMyType "foo;name;bar;baz;1,23;4,56"
val it : Result<MyType,string> = Ok {Name = "name";
Coordinate = {Longitude = 1.23;
Latitude = 4.56;};}
> parseMyType "foo"
val it : Result<MyType,string> = Error "Not enough parts"
> parseMyType "foo;name;bar;baz;badnumber;4,56"
val it : Result<MyType,string> = Error "Not a number: badnumber"
person
Fyodor Soikin
schedule
30.07.2017