Pull to refresh

Три парадигмы F#

Programming *F# *

Введение


Все, кто так или иначе связан с .NET программированием знает, что уже в следующую версию Visual Studio будет встроен новый язык программирования — F#, который позиционируется как функциональный, чем сразу, так уж повелось, вызывает подозрения в бесполезности. Для того, чтобы показать, что F# — куда больше, чем просто ФЯП (хотя и просто ФЯП — это очень немало), я и написал все нижеследующее.
Эта статья, несмотря на изрядную длину, не претендует на то, чтобы полностью описать всю функциональность языка. Это всего лишь краткий обзор, призванный продемонстрировать широкий спектр возможностей, каждая из которых заслуживает отдельной статьи, и даже не одной.
Кроме того, написав такой пространный пост, я хотел сделать задел на будущее, чтобы в дальнейшем мне не отвлекаться на незначительные вещи базового уровня. Конечно, сразу головой в пруд — это действенно, но и какой-никакой фундамент не помешает.
А уже в следующий раз я приведу пример на волнующую тему пригодности F# для обычной профессиональной программистской деятельности.
И еще раз, под катом действительно МНОГО текста. И не говорите потом, что я вас не предупреждал. =)

F# функциональный


Конечно, в первую очередь F# — функциональный язык, а значит именно поддержка функциональной парадигмы в нем реализована наиболее полно. Как известно, много ключевых слов и литералов в нем заимствовано из OCaml, что неудивительно, так как Дон Сайм (Don Syme), главный создатель F# когда-то приложил руку и к OCaml.
Много знаний о F#, как о чистом функциональном языке программирования читатель мог почерпнуть уже из прошлых моих постов, однако исключительно ради того, чтобы создать полное впечатление о языке, я кратко повторю все их еще раз.

Идентификаторы, ключевые слова, функции.


Итак, F#, как ни странно, позволяет программисту определять идентификаторы, с помощью которых можно будет в последствии обращаться к функциям. Делается это с помощью ключевого слова let, за которым следует имя идентификатора, список параметров, а после знака равенства — выражения, определяющего функцию. Примерно так:
    let k = 3.14
    let square x = x**2.0

В отличие от императивного программирования, первое выражение определяет не переменную, а скорее константу, так как значение ее нельзя изменять во время выполнения программы. Вообще говоря, F# не делает различия между функциями и значениями — любая функция является значением, которое так же свободно можно передавать в качестве параметра.
Список всех ключевых слов F# можно увидеть здесь. Слова из второго приведенного по ссылке списка не используются в данный момент, но зарезервированы на будущее. Их можно использовать, но компилятор при этом выдаст предупреждение.
F# поддерживает каррированные функции, в которые можно передавать не все параметры сразу:
    let add a b = a + b //'a -> 'a -> 'a
    let addFour = add 4 //'a -> 'a

Второй идентификатор задает функцию уже от одного свободного параметра, другой определен как 4. Это еще раз демонстрирует тезис, что функция есть значение. Поскольку функция — значение, то не получив полного набора параметров, она попросту возвращает другую функцию, которая тоже является значением.
Однако все функции из .NET не обладают свойством каррируемости, и для их применения в F# используются кортежи — наборы нескольких разнотипных значений. Кортеж может содержать множество различных параметров внутри себя, однако рассматривается F# как один параметр, и как следствие применяется только целиком. Записываются кортежи в круглых скобках, через запятую.

    let add (a,b) = a + b
    let addFour = add 4

Такой код не будет скомпилирован, так как по мнению F# мы пытаемся применить функцию к параметру несоответствующего типа, а именно int вместо 'a * 'b.
Однако следует помнить, что при разработке собственных функций, особенно тех, которые будут использоваться другими программистами, следует по возможности делать их каррируемыми, так как они очевидно обладают большей гибкостью в использовании.
Как я полагаю, читатель уже заметил, в F# в функциях не нужно явно определять возвращаемое значение. Однако при этом непонятно, как вычислять промежуточные значения внутри функции? Здесь F# использует способ, о существовании которого многие, думаю, успели подзабыть — с помощью пробелов. Внутренние вычисления в функции обычно отделяются четырьмя пробелами:
    let midValue a b =
        let dif = b - a
        let mid = dif / 2
        mid + a

Кстати, если кого-то из видевших программы на F# удивляло постоянное присутствие в коде команды #light, то один из ее эффектов как раз и заключается в том, что пробелы становятся важны. Это позволяет избежать использования множества ключевых слов и знаков, пришедших из OCaml, таких как in, ;;, begin, end.
Каждый из идентфикаторов имеет свою область применения, которая начинается от места его определения (то есть применять его выше по коду, чем место его определения, нельзя), а заканчивается в конце секции, где он был определен. Например, промежуточные идентификаторы dif и mid из предыдущего примера не будут действовать за пределами функции midValue.
Идентификаторы, определенные внутри функций имеют некоторую особенность в сравнении с теми, что определены на внешнем уровне — они могут быть переопределены с помощью слова let. Это полезно, так как позволяет не изобретать все новые, и чаще всего мало что значащие имена для держания промежуточных значений. Например, в предыдущем примере мы могли бы написать так:
    let midValue a b =
        let k = b - a
        let k = k / 2
        k + a

Более того, поскольку это переопределение в полном смысле, а не изменение значения переменной, то мы вполне можем поменять не только значение идентификатора, но и его тип.
    let changingType () =
        let k = 1
        let k = "string"

F# позволяет в большинстве случаев обходиться вовсе без циклов за счет пакетных функций обработки последовательностей map, list, fold и.т.д., однако в тех случаях, где это необходимо, можно использовать рекурсию. Что легче для понимания, цикл или рекурсия — вопрос в целом открытый, на мой взгляд и то и другое вполне посильно. Для того, чтобы функция в F# могла обратиться к себе внутри своего определения, необходимо добавить после let ключевое слово rec.

F# является сильно типизированным языком, то есть нельзя использовать функции с значениями неподходящего типа. Функции, как и любые значения, имеют свой тип. F# во многих случаях сам выводит тип функции, при этом он может быть определен неоднозначно, например:
    let square x = x*x

имеет тип 'a -> 'a, где 'a может быть int, float, и вообще говоря любым, для которого перегружен оператор *.
При необходимости тип параметра функции можно задать самому (например, когда надо использовать методы класса):
    let parent (x:XmlNode) = x.ParentNode


Лямбды и операторы


F# поддерживает анонимные функции или лямбды, которые используются, если нет необходимости присваивать функции имя, когда она передается в качестве параметра для другой функции. Пример лямбды ниже:
    List.map (fun x -> x**2) [1..10]

Данная функция выдаст список, состоящий из квадратов всех чисел от одного до десяти.
Кроме того, в F# существует и еще один способ определения лямбды с помощью ключевого слова function. Определенная таким образом лямбда может содержать внутри себя операцию сравнения с шаблоном (pattern matching), однако она принимает только один параметр. Но даже и в этом случае можно сохранить каррируемость функции:
    function x -> function y -> x + y

Лямбды в F# поддерживают замыкание, однако об этом будет подробнее рассказано во второй части обзора.
В F# операторы (унарные и бинарные) можно рассматривать как более эстетичный способ вызова функций. Так же как и в C#, операторы перегружены, так что могут использоваться с различными типами, однако в отличие от C#, здесь нельзя применять оператор к операндам различного типа, то есть нельзя складывать строки с числами (и даже целые с вещественными), необходимо всегда делать приведение.
F# позволяет перегружать операторы, или определять собственные.
    let (+) a b = a - b
    printfn "%d" (1 + 1) // "0"

Операторы могут являться любой последовательностью следующих символов !$%&*+_./<=>?@^|~:
    let (+:*) a b = (a + b) * a * b
    printfn "%d" (1 +:* 2) // "6"


Инициализация списков


Еще одной мощной техникой F# является инициализация списков, которая позволяет создавать достаточно сложные списки, массивы и последовательности (эквивалент IEnumerable) напрямую, с помощью использования специального синтаксиса. Списки задаются в прямоугольных скобках [ ], последовательности — в {}, массивы — в [| |].
Простейший способ — определение промежутка, который задается с использованием (..), например:
    let lst = [1 .. 10]
    let seq = {'a'..'z'}
    

Также с помощью добавления еще одного (..) можно задавать шаг выбора в промежутке:
    let lst = [1 .. 2 .. 10] // [1, 3, 5, 7, 9]

Кроме того, при создании списков можно использовать циклы (циклы могут быть как одинарными, так и вложенными в любой степени)
    let lst = [for i in 1..10 -> i*i] // [1, 4, 9,..]

Однако и это еще не все. При инициализации списков можно явно указывать, какие элементы заносить, с помощью операторов yield (добавляет в последовательность один элемент) и yield! (добавляет множество элементов), а также можно использовать любые логические конструкции, циклы, сравнения с шаблоном. Например, вот так выглядит создание последовательности имен всех файлов содержащихся в данной папке и во всех ее подпапках:
    let rec xamlFiles dir filter =
        seq { yield! Directory.GetFiles(dir, filter)
            for subdir in Directory.GetDirectories(dir) do yield! xamlFiles subdir filter}


Сравнение с шаблоном


Сравнение с шаблоном немного похоже на обычный условный оператор или switch, однако обладает намного большей функциональностью. В общем виде синтаксис операции выглядит так:
    match идент with
    [|]шаблон1|шаблон2|..|шаблон10 -> вычисление1
    |шаблон11 when условие1 -> вычисление2
    ...

Сравнение с шаблонами идет сверху вниз, так что не следует забывать о том, что более узкие шаблоны должны располагаться выше. Самый общий шаблон выглядит так: _ (нижнее подчеркивание), и означает, что нас не интересует значение идентификатора. Кроме того, сравнение с шаблоном должно быть полным (отсутствуют нерассмотренные возможности) и все вычисления должны выдавать результат одного и того же типа.
Простейший вид операции с шаблоном сравнивает идентификатор с некоторым значением (числовым, строковым) С помощью ключевого слова when к шаблону можно добавлять условие, так что вычисления будут выполняться
Если вместо значения подставляется другой идентификатор, то ему присваивается значение проверяемого идентификатора.
Наиболее часто используемые варианты сравнения с шаблоном — над кортежами и списками. Пусть x — кортеж вида (string*int), тогда возможно написать любой подобный шаблон:
    match x with
    | "Пупкин", _ -> "Здравствуй, Вася!"
    | _, i when i > 200 -> "Здравствуй, Дункан!"
    | name, age -> sprintf "Здравствуйте %s, %d" name age
    | _ -> "И вам тоже здрасте"

Заметьте, что если в шаблоне имеются идентификаторы, то они автоматически определятся соответствующими значениями, и в обработке можно использовать отдельно поля name и age.
Точно таким же образом обрабатывается список (который на самом деле и не список даже, а размеченное объединение (discriminated union), о которых речь ниже). Обычно шаблоны для списка ('a list) выглядят либо как [], если он пустой, либо head::tail где head имеет тип 'a, а tail — 'a list, однако возможны и другие варианты, например:
    match lst with
    |[x;y;z] -> //lst содержит три элемента, причем они присвоятся идентификаторам x y z.
    |1::2::3::tail -> // lst начинается с [1,2,3] tail присвоится хвост списка

Способность при сравнении с шаблоном передавать в идентификаторы значения так полезна, что в F# существует возможность такого присвоения напрямую, без использования шаблонного синтаксиса, вот так:
    let (name, age) = x

или даже так:
    let (name, _) = x

если нас интересует только первый элемент кортежа.

Записи


Записи (record) в F# аналогичны кортежам, с той разницей, что в них каждое поле имеет название. Определение записи заключается в фигурные скобки и разделяется точкой с запятой.
    type org = { boss : string; tops :string list }
    let Microsoft = { boss = "Bill Gates"; tops = ["Steve Balmer", "Paul Allen"]}

Доступ к полям записи осуществляется, как обычно, через точку. Записи могут имитировать классы, что будет показано ниже.

Размеченное объединение


Этот тип в F# позволяет хранить данные, имеющие разную структуру и смысл. Например, вот такой тип:
    type Distance =
    |Meter of float
    |Feet of float
    |Mile of float
    
    let d1 = Meter 10
    let d2 = Feet 65.5

Хотя все три вида данных имеют один и тот же тип (что необязательно), они очевидно отличны по смыслу. Обработка размеченных объединений всегда осуществляется через сравнение с шаблоном.
        match x with
        |Meter x -> x
        |Feet x -> x*3.28
        |Mile x -> x*0.00062

Как уже говорилось, такой распространенный тип данных как список, является размеченным объединением. Неформальное его определение выглядит так:
    type a' list =
    |[]
    |:: of a' * List

Кстати, как заметно из вышеприведенного примера, размеченные множества в F# можно параметризовать, на манер generic'ов.

F# императивный



Тип unit


Тип unit родственен типу void из C#. Если функция не принимает аргументов, то ее тип входа — unit, если она не возвращает никакого значения — ее тип выхода unit. Для функционального программирования функция, которая не принимает или не возвращает значение не представляет никакой ценности, однако в императивной парадигме она ценность имеет за счет побочных эффектов (например ввода-вывода) Единственное значение типа unit имеет вид (). Вот такая функция ничего не принимает и ничего не делает (unit -> unit).
    let doNothingWithNothing () = ()

Скобки после названия означают, что это именно функция с пустым входом, а не значение. Как мы уже говорили функции являются значениями, но между функциональным и нефункциональным значением есть большая разница — второе вычисляется только однажды, а первое — при каждом вызове.
Любую функцию, возвращающую значение можно конвертировать в функцию, возвращающую тип unit с помощью функции ignore. Применяя ее, мы как бы сообщаем компилятору, что в данной функции нас интересует только побочный эффект, а не возвращаемое значение.

Ключевое слово mutable


Как мы знаем, в общем случае, идентификаторы в F# можно определить каким-то значением, однако это значение нельзя изменить. Однако все-таки старые добрые императивные переменные бывают полезны, так что в F# предусмотрен механизм для создания и использования переменных. Для этого перед именем переменной надо написать ключевое слово mutable, а изменять значение можно с помощью оператора <-.
    let mutable i = 0
    i <- i + 1

Однако применение таких переменных ограничено, например их нельзя использовать во внутренних функциях, а также для замыкания в лямбдах. Такой код выдаст ошибку:
    let mainFunc () =
        let mutable i = 0
        let subFunc () =
            i <- 1


Тип ref


В F# существует и другой способ определения переменных, с помощью типа ref. Для этого всего лишь надо поставить ключевое слово ref перед вычислениями, которые представляют значение идентификатора.
Для того, чтобы присвоить переменной другое значение используется до боли ностальгичный оператор :=, обращение же ко значению переменной осуществляется добавлением! перед именем переменной.
    let i = ref 0
    i := !i + 1

Пожалуй данная нотация далеко не столь опрятна, как предыдущая, чего стоит только использование восклицательного знака для получения значения (для отрицания в F# существует ключевое слово not)
Однако в отличие от mutable, у ref типа нет ограничений на область действия, так что его можно использовать и во вложенных функциях, и в замыканиях. Такой код будет работать:
    let i = ref 2
    let lst = [1..10]
    List.map (fun x -> x * !i) lst


Массивы


В F# существуют массивы, которые являются изменяемым типом. Значения внутри массива можно переприсвоить, в отличие от значений в списках. Массивы задаются в таких скобках [| |], элементы в нем перечисляются через точку с запятой. Доступ к элементу массива осуществляется через .[ind], а присваивание — знакомым по работе с mutables оператором <-. Все функции для обработки массивов (практически аналогичные методам для обработки списков), находятся в классе Array.
    let arr = [|1; 2; 3|]
    arr.[0] <- 10 // [|10,2,3|]

Массивы можно инициализировать точно таким же способом, как и списки, используя .., yield и.т.п.
        let squares = [| for x in 1..9 -> x,x*x |] // [| (1,1);(2,4);...;(9,81) |]

Также F# позволяет создавать многомерные массивы, как «ступенчатые» (с подмассивами разной длины), так и «монолитные».

Управляющая логика


В F# можно использовать привычную императивную управляющую логику — условный оператор ifthenelse, а также циклы for и while.
Следует помнить о том, что оператор if тоже можно рассматривать как функцию, а значит она должна при любом условии выдавать значение одного и того же типа. Это к тому же предполагает, что использование else обязательно. На самом деле, есть одно исключение — когда вычисления при выполненном условии возвращают тип unit:
    if System.DateTime.Now.DayOfWeek = System.DayOfWeek.Sunday then
        printfn "Хороших выходных!"
    printfn "Каждый день замечателен!"

Для определения, какие функции относятся к циклу, какие нет, также используются сдвиги. Например, в верхнем примере второе предложение будет выведено независимо от дня недели.
Цикл for в F# имеет тип unit, так что вычисления в теле цикла должны выдавать этот тип, иначе компилятор выдаст предупреждение.
    let arr = [|1..10|]
    for i = 0 to Array.length arr - 1 do
        printfn arr.[i]

Если хочется пройтись в обратную сторону, то to заменяется на downto, как в старые добрые времена.
Также можно использовать другую форму цикла for, аналогичную всем знакомому foreach:
    for item in arr
        print_any item

Цикл while также вполне обычен и знаком для императивного программиста, тело его располагается между ключевыми словам do и done, но второе можно опционально опускать, используя систему сдвигов.

Вызов статических методов и объектов из библиотек .NET


В F# можно использовать весь набор инструментов .NET, однако очевидно, что все методы, написанные не под F# не обладают свойством каррируемости, так что аргументы им надо задавать в виде кортежа, соответствующего по типу набору входных элементов. При этом запись вызова не будет ни на йоту отличаться от сишарпной:
    #light
    open System.IO
    if File.Exists("file.txt") then
        printf "Есть такой файл!"

Однако, если вам так уж хочется, чтобы .NET метод обладал бы каррируемостью, его надо импортировать, примерно следующим образом:
    let exists file = File.Exists(file)

Использовать объекты так же просто — они создаются с помощью ключевого слова new (кто бы мог подумать?), и применением соответствующего кортежа параметров конструктора. Объект можно присвоить идентификатору с помощью let. вызов методов аналогичен статическим, поля изменяются с помощью <-.
    #light
    let file = new FileInfo("file.txt")
    if not file.Exists then
        using (file.CreateText()) (fun stream ->
            stream.WriteLine("Hello world"))
    file.Attributes <- FileAttributes.ReadOnly
    

F# позволяет инициализировать поля сразу при создании объекта, вот таким образом:
        let file = new FileInfo("file.txt", Attributes = FileAttributes.ReadOnly)


Использование событий в F#


У каждого события в F# существует метод Add, который добавляет функцию обработчика к событию. Функция обработчика должна иметь тип 'a -> unit. Вот как можно подписаться на событие таймера:
    #light
    open System.Timers
    let timer = new Timer(Interval=1000, Enabled=true)
    timer.Elapsed.Add(fun _ -> printfn "Timer tick!")

Отписка от события производится с помощью метода Remove.

Оператор |>


Пересылающий оператор |> определяется следующим образом:
    let (|>) f g = g f

Он передает первый аргумент в качестве параметра второму аргументу. Второй аргумент, разумеется должен быть функцией, которая в качестве единственного параметра принимает значение типа f. Кстати, именно из-за возможности использования с пересылающим оператором все функции над списками (iter, map, fold) принимают сам список последним. Тогда в качестве g можно использовать недоопределенную функцию:
    [1..10] |> List.iter (fun i -> print_int i)

Например, функция iter имеет вид ('a list -> unit) -> 'a list -> unit, задав лямбдой первый параметр мы получаем функцию типа 'a list -> unit, которая как раз принимает в качестве аргумента определенный до оператора список.
В программах зачастую применяются длинные цепи пересылающих операторов, каждый из которых обрабатывает значение, полученное предыдущим, этакий конвеер.

F# объектно-ориентированный


Думаю, мало кто готов поспорить с тем, что именно объектно-ориентированная парадигма на сегодняшний день является флагманом программирования, и конечно же, F# не мог проигнорировать заложенные в нем концепции. Посмотрим, что же он нам предлагает.

Типизация.


В F# имеется возможность явно изменять статический типа значения. У F# для приведения вверх и вниз используются два разных оператора. Приведение вверх, то есть присвоение статическому типу значения типа одного из его предков, осуществляется оператором :>. Значение strObj в нижнем примере будет иметь тип object.

    let strObj = ("Тили-тили, трали-вали" :> obj)

Присвоение вниз, то есть уточнение типа значения типом одного из его потомков, осуществляется оператором :?>.
Для проверки типа значения (аналог is из C#) служит оператор :?, который можно использовать не только в логических конструкциях, но и при сравнении с шаблоном.
    match x with
    |:? string -> printf "Это строка!"
    |:? int -> printf "Это целое!"
    |:? obj -> printf "Неизвестно что!"
    

Обычно F# не берет при вычислении функций в расчет иерархию наследования типов, то есть не позволяет применять в качестве аргумента тип-наследник. Например такая программа не скомпилируется:
    let showForm (form:Form) =
        form.Show()
    let ofd = new OpenFileDialog();
    showForm ofd

В принципе, можно явно привести тип: showForm (ofd :> Form), однако F# предоставляет и другой способ — добавить перед типом в сигнатуре функции знак решетки #.
    let showForm (form: #Form) =
        form.Show()

Таким образом определенная функция примет аргументом объект любого наследуемого от Form класса.

Записи и обединения как объекты


В записи и объединения можно добавить методы. Для этого после определения записи необходимо добавить ключевое слово with, после определения всех методов написать end, а перед идентификатором каждого метода использовать ключевое слово member:
    type Point ={
        mutable x: int;
        mutable y: int; }
    with
        member p.Swap() =
            let temp = p.x
            p.x <- p.y
            p.y <- temp
    end

Заметьте, что параметр p, заданный перед именем метода используется внутри него для получения доступа к полям.

Классы и интерфейсы


Классы в F# определяются с помощью ключевого слова type, за которым следует имя класса, знак равенства и ключевое слово class. Завершается определение класса ключевым словом end. Для того, чтобы задать конструктор, необходимо в определение класса включить член с именем new.
    type construct = class
        new () = {}
    end
    let inst = new construct()

Обратите внимание, что определение класса должно содержать в себе хотя бы один конструктор, иначе код не скомпилируется! F# не предоставляет конструктора по умолчанию, как C#.
Чтобы определить поле, необходимо добавить перед его именем ключевое слово val.
    type File = class
        val path: string
        val info : FileInfo
        new () = new File("default.txt")
        new (path) =
            { path = path;
             info = new FileInfo(path) }
    end
    let file1 = new File("sample.txt")

Как видите, конструкторы можно совершенно привычным образом перегружать. Конструктор не может оставить некоторое поле неинициализированным, иначе код не будет скомпилирован. Заметьте, что в конструкторах можно только инициализировать поля или вызывать другие конструкторы. Чтобы задать в конструкторе дополнительные операции, необходимо дописать после него then, после которого записать все дополнительные вычисления:
    new (path) as x =
        { path = path;
         info = new FileInfo(path) }
        then
        if not x.info.Exists then printf "Нет файла!"

По умолчанию поля класса неизменяемы, чтобы сделать некоторое поле изменяемым, необходимо добавить перед его именем mutable.
Интерфейс в F# задается и имплементируется следуюшим образом:
    let ISampleInterface = interface
        abstract Change : newVal : int -> unit
    end
    
    type SampleClass = class
        val mutable i : int
        new () = { i = 0}
        interface ISampleInterface with
            member x.Change y = x.i <- y
        end
    end

F# предлагает еще один элегантный способ определения класса — неявное задание. Сразу после названия класса перечисляются входные параметры, которые в ином случае входили бы в аргументы конструктора. Конструирование класса происходит прямо в его теле, с помощью последовательности let, предшествующих определению методов. Все таким образом определенные идентификаторы будут приватны для класса. Поля и методы класса задаются с помощью ключевого слова member. Лучше сразу посмотреть пример:
    type Counter (start, inc, length) = class
        let finish = start + length
        let mutable current = start
        member c.Current = current
        member c.Inc () =
            if current > finish then failwith "Динь-дилинь!"
            current <- current + inc
    end

    let count = new Counter(0, 5, 100)
        count.Inc()

F# как и C# поддерживает только одиночное наследование, и имплементацию нескольких интерфейсов. Наследование задается с помощью ключевого слова inherit которое идет сразу после class:

    type Base = class
        val state : int
        new () = {state = 0}
    end
    
    type Sub = class
        inherit Base
        val otherState : int
        new () = {otherState = 0}
    end

Нет необходимости вызывать явно базовый пустой конструктор. При вызове любого конструктора потомка автоматически вызовется пустой конструктор предка. Если такого конструктора в предке нет, необходимо явно вызвать в теле конструктора потомка базовый конструктор с помощью ключевого слова inherit.
Свойства в F# определяются следующим образом:
    type PropertySample = class
        let mutable field = 0
    member x.Property
        with get () = field
        and set v = field <- rand
    end

Для определения статических полей перед member добавляется ключевое слово static и убирается параметр, обзначающий экземпляр класса:
    type StaticSample = class
        static member TrimString (st:string) = st.Trim()
    end


Заключение



Сегодня мы кратко рассмотрели большинство базовых возможностей языка, и можно на основании увиденного сделать какие-то выводы.
Что ж, по сути, любая операция с классами и переменными, доступная в C#, может быть выполнена и в F#, так что этот язык ничуть не менее объектно-ориентирован, чем его старший брат. Синтаксис в нем возможно и сложнее, но не критически, и как кажется, всего лишь вопрос привычки. С другой стороны, в функциональном плане F#, как и следовало ожидать (на самом деле следует ожидать еще больше, поскольку здесь была описана
Tags:
Hubs:
Total votes 43: ↑40 and ↓3 +37
Views 20K
Comments 68
Comments Comments 68

Posts