Три парадигмы 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#, как и следовало ожидать (на самом деле следует ожидать еще больше, поскольку здесь была описана
    Поделиться публикацией

    Похожие публикации

    Комментарии 68
      0
      Спасибо за статью.

      Кажется, что «Размеченное объединение» принято называть алгебраическим типом данных.

      В F# можно использовать весь набор инструментов .NET, однако очевидно, что все методы, написанные не под F# не обладают свойством каррируемости


      Почему очевидно?
        0
        ну потому что они были написаны задолго до появления в F# и полагаю, задолго до того, как разработчики .NET даже подумали о том, что он может появится. Без функциональной парадигмы каррируемость совершенно бесполезна, потому что в том же C# функции (методы) вовсе не являются значениями. Там для этого выделен специальный класс Action и делегаты собственно, но это все костылики =))
          0
          Потому, что есть, например, перегрузка методов по типу и количеству параметров. то есть если написано:

          Class.test b

          где b — String

          Непонятно, что имеется ввиду:
          — метод test(string, string) -> int и надо вернуть string -> int
          — метод test(sting) -> int и надо вернуть int

          таким мобразом .NET методы фактически получают один параметр — кортеж

            0
            Good, но не совсем — можно было бы использовать вывод типов для разруливания таких проблем.
              0
              >можно было бы использовать вывод типов для разруливания таких проблем.

              Как?
              let i = test «Hello»
              Как определить, i должно иметь тип int или string → int?

              >Кажется, что «Размеченное объединение» принято называть алгебраическим типом данных.

              Алг. типы данных — более общее понятие. Можно сказать так: алг. типы данных в F# реализованы в терминах размеченных объединений (discriminated unions).
                0
                По дальнейшему использованию=)

                Если дальше в коде написано let j = i + 5, то тип i — int, а если написано let j = i «qwerty», то string → int.
                  0
                  >По дальнейшему использованию=)

                  F# строго типизированный язык.

                  >Если дальше в коде написано let j = i + 5, то тип i — int, а если написано let j = i «qwerty», то string → int.

                  Дальнейшее использование может быть слишком «дальнейшим». Например, использование может быть в клиентском коде, т. е. в момент компиляции test «Hello» ничего не известно о том, как результат такого вызова будет использоваться. Грубо говоря, библиотечная функция, возвращающая test «Hello» может быть в одной dll'ке, а твой let j = i + 5 — в другой. Первая dll'ка ничего не должна знать о второй.
                    0
                    Вывод типов не противоречит строгой типизации.

                    Второй частью вы затрагиваете вопрос, который не был описан в статье — разбиение программы на модули. Например, является ли глобальная константа let j = 1 доступной вне сборки.

                    В любом случае эту неоднозначность можно разрешить, если обязать программиста фиксировать тип интерфейса глобальных публичных объектов, таких как сигнатура функции, interface, класс или константа, а вывод типов использовать во всех оставшихся случаях.
                      0
                      >Вывод типов не противоречит строгой типизации.

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

                      >… если обязать программиста фиксировать тип интерфейса глобальных публичных объектов.

                      Это противоречит твоему желанию «[определить тип] по дальнейшему использованию=)»
                        0
                        Но противоречит, если тип должен определяться контекстом будущего использования


                        Если ограничить вывод типов компилируемым модулем, то противоречие снимается. Этого можно достичь, если зафиксировать тип объектов, которые видны вне модуля.

                        Мое желание указывать тип как можно меньше (но оставаться в рамках строгой типизации) и работать с .net кодом так, что бы имея дело со сборкой не иметь ввиду на каком языке она написана, то есть хочу однородности. Я не против того, что бы указать тип публичных объектов сборки, но обычно большая часть кода приходиться на тело методов/функций, поэтому хорошо бы иметь там вывод типов.

                        Это сделать реально, в качестве примеров можно привести haskell, кроме того это реально сделать и для .NET — существует Nemerle. Например, в нем можно создать generic Dictionary без указания параметров типов — компилятор сам их выведет по его использованию.
                          0
                          Модули и сборки не при чём. Вопрос в неоднозначности выбора варианта перегрузки и в неоднозначности выбора перегруженная-функция/каррированная-функция.

                          Предположим, есть две перегрузки некоторой функции:
                          SomeFunc: (int) → SomeClass
                          SomeFunc: (string → int) → SomeClass

                          let i = test «Hello» // Тип int или string → int, если был бы «вывод типов шиворот навыворот».
                          let j = SomeFunc i // Какая перегрузка должна вызваться?

                          Обычно в современных языках тип выражения слева от «=» определяется по типу выражения справа от «=». Ты же предлагаешь наоборот — значение типа справа от «=» выбирать по будущему типу выражения слева от «=».
                            0
                            Теперь все ОК: каррированию мешает отсутствие мощной системы вывода типов, а ей мешает наличие возможности перегружать методы.

                            Хотя мне кажется, что такой код встречается не часто и избежать неоднозначности можно явно объявлять тип там, где его не может вывести компилятор. То есть писать что-то вроде let i: string → int = test «Hello».
                          0
                          Но противоречит, если тип должен определяться контекстом будущего использования.

                          import Control.Concurrent
                          
                          main = do
                              c <- newChan     -- newChan :: Chan a
                              writeChan c 10   -- уточняем до Num a => Chan a
                              writeChan c 2.6  -- всё ещё нормально, уточняем до Fractional a => Chan a
                              writeChan c "ab" -- врёшь, не пройдёшь!
                          

                          Внезапно, тип выражения стал зависимым от дальнейшего использования. В до жути строго типизированном языке.
                            0
                            — newChan :: Chan a

                            IO (Chan a) конечно же, прошу прощения.
                            В OCaml такой эффект можно наблюдать с Hashtbl.t.
                          0
                          да, все вопросы видимости-невидимости остались за пределами, и так уже многовато вышло ) Как-нибудь в другой раз.
                          на самом деле все таким образом определенные идентификаторы (что функции, что значения) можно видеть из другой сборки, установив референс.
                          Можно определить module или namespace в файле, но если они не определены, то считается, что имя файла = имя модуля.
              +5
              шикарная статья, но где-то посередине у меня сломался мозг. Что это, неприятие сознанием ФП или просто вечерняя усталость, сказать затрудняюсь. Было бы отлично воспринимать такую информацию каплями, а не бурным потоком. Но все равно — спасибо. Буду считать вашу статью введением в язык.
                0
                ну я примерно с этой целью ее и писал — как введение.
                Я конечно уже апостериори думал поделить ее на три, но поскольку изначально мысль была именно показать рядом все три стороны, то уж решил будь как будет.
                  0
                  Неприятие ФП. Средний императивный прогер достигает нирваны начинает понимать ФП после 5-10 часов медитирования над своим кодом в Haskell/F#/OCaml.
                    0
                    Ага а еще в Lisp, АФС и даже Perl )
                      0
                      Из всех виденных мной языков сложнее всех давался Prolog. Даже Lisp по сравнению с ним прост в понимании.

                      Ах да, и ещё есть J, который я не осилил до сих пор. rosettacode.org/wiki/Spiral#J
                        0
                        Да Prolog просто взламывает мозг своим синтаксисом после обычного программирования.
                  0
                  Хорошая статья. Прочитал с удовольсвием, спасибо.
                    +2
                    Статья хорошая, но язык показался непродуманным: разный синтаксис в похожих ситуациях, недодуманный синтаксис, специальный синтаксис для ситуаций, которые должен разрешать интерпретатор/компилятор и т. д.

                    Например, если сравнивать с Python, то я выберу Python.
                      0
                      возможно в чем-то вы правы, но во-первых это язык еще развивается, и последняя версия — всего лишь CTP. Так что возможно кое-какие шероховатости исправят.
                      Питон сам по себе на данный момент наверняка более вылизан и продуман, однако пара F#- .NET CLR выглядит очень перспективно имхо.
                        0
                        В F# я вижу мало отличий от OCaml.
                          0
                          Возразить особо нечего. Ну так это ведь не тайна за семью печатями, что F# — новый «сын» ML серии. Однако и нельзя на этом основании делать вывод, что он бесполезен, поскольку есть OCaml.
                          • НЛО прилетело и опубликовало эту надпись здесь
                          0
                          История равзития семейства ML несколько более чем у Python.
                          Так что я не согласен с тем, что синтаксис непродуманный.
                            0
                            В каком он месте не продуман? В питоне даже лямбда только однострочная, и это только из-за упрощения синтаксиса.
                              0
                              В Питоне масса недостатков и однострочная лямбда — далеко не самый тяжёлый, но тут их целая куча. Точнее, у меня ощущение, что синтаксис целиком непродуман.
                                0
                                Не, ну сахера конечно много но в целом вполне нормальный синтаксис.
                              0
                              >разный синтаксис в похожих ситуациях
                              Мне после лиспа так любой синтаксис кажется перегруженным и усложнённым :)
                                0
                                С другой стороны этот синтаксис можно довольно легко прочитать, а чтобы прочитать более-менее длинную программу на лисп или хаскелл, нужно кажется постичь дзен. =)
                                0
                                бугога… питон сравнивать с F#/окамл :D
                                есть задачи и критерии для задач, при которых ЯП выбирается не исходя из «продуманного и простого синтаксиса»

                                посмотрите например тесты по производительности вычислений и все поймете — что питон один из тормозных языков и несмотря на его красивость я бы выбрал скриптовоя язык lua с jit

                                ну а окамлу и F# конкуренты только другие фп языки.
                                  0
                                  Я смотрю тесты. По моим сведениям Python один из самых производительных скриптовых языков.
                                    0
                                    В том-то и дело, что скриптовый. GIL и reference counting вместо нормальной сборки мусора это конечно да…
                                    0
                                    Кстати, с OCaml я Python не сравниваю, я сравниваю с F#. Причём, я знаю приличное количество языков, чтобы видеть, что синтаксис F# не продуман.
                                  0
                                  Да… Автор не обманул. Почитаю позже, потому что заинтересовало. Спасибо за статью.
                                    0
                                    Вопрос автору.

                                    Однажды прочитал мнение, что F# приводит к тому, что программист больше думает, меньше программирует.

                                    Я сейчас смотрю на эти программки и читается текст очень тяжело. Может потому, что я к синтаксису не привык?
                                    Вот я вижу, вы уже изрядно попользовались F#.

                                    На ваш взгляд, что легче будет: прочитать сложный алгоритм, реализованный на процедурном (ооп) языке программирования или на функциональном?
                                      +1
                                      Знаете, я скажу так: это дело привычки, что там, что там. На ФЯП во многих случаях код получается значительно короче и декларативней, чем на ООЯП — это правда. Но не всегда.
                                      А вообще любой сложный код читается хорошо в первую очередь когда есть комменты, без них и в сишном коде закопаться легче легкого :)
                                      0
                                      Чем дальше в лес, тем больше у MS языков программирования новых
                                        0
                                        не припомню так уж много новых ЯП у MS. Вроде уже x последних лет упорно держатся за C#.
                                        0
                                        трудно будет людям, которые захотят начать программировать
                                          0
                                          могу сказать по личному опыту — далеко не так, как кажется на первый взгляд. На поверку все оказывается довольно логично.
                                          0
                                          Мне в настоящий момент не очень нравиться как компилятор F# генерирует CIL-код. Например, если в классе F# создать поле, то ILDASM показывает, что кроме поля генерируються get и set свойства, при этом модификаторы доступа к полю (имеется ввиду CIL поле) указать в F# нельзя, в итоге, в C# можно напрямую обращаться к полям, хотя доступны и свойства, просто по умолчанию у поля почему-то public — прямое нарушение принципа инкапсуляции. А если в F# к полю создать свойства, как в примере выше, то из C# вообще будет видно 4 свойства и 1 поле. Хотя, наверное, к релизу подправят.
                                          Тяжкий комент получился.
                                            0
                                            Лишний код в CIL наверно поправят, насчет видимости — в F# есть все те же самые модификаторы private, inline, public, только порядок другой — здесь модификатор должен непосредственно предшествовать идентификатору.
                                            Написав val mutable private prVal: string получим изменяемое поле видимое только внутри класса. То же самое с member, причем модификаторы как и в сишарп можно ставить и перед get и set отдельно, если есть необходимость.
                                            Почему по умолчанию поля члены класса сделаны публичными — могу только догадываться. Вроде в Object Pascal так же было (вспоминая школьные годы =))
                                            0
                                            После Nemerle всё читается легко и понятно :) Хотя, конечно, Nemerle бы увидеть в VS 2010 на месте F#, я бы за такое дорого дал. Но — что уж поделать. Тяжело расставаться с фигурными скобочками ;-) Я бы лучше уж begin\end писал, как в старые добрые времена, чем на пробелы завязываться.
                                              0
                                              Мне кажется что Nemerle тоже рассматривался в качестве прототипа. Но отдали предпочтение Ocaml.
                                              А писать Begin end, ставить ;; в конце операторов и использовать функции внутри друг друга с помощью in вполне можно и сейчас, #light не отключает эту возможность, а всего лишь добавляет другую. Ну в крайнем случае можно эту директиву стереть, чтобы не смущала :)
                                                0
                                                да, я понял, что не исключает)
                                                не знаете, кстати, почему отдали предпочтение Ocaml? Ведь Nemerle гораздо более «C# friendly»-язык, порог вхождения для был бы меньше и т.п.
                                                  0
                                                  * для => для существующих C#-программистов
                                                    0
                                                    Честно отвечу — не знаю. Я Nemerle смотрел в свое время, но не так сильно, чтобы мог хоть с какой-то убедительностью пытаться их сравнивать.
                                                0
                                                А у меня вот какой вопрос — из чего состоит область промышленного применения ФЯП? Вообще, для чего F# нужен, кроме того, что он используются в реализации математических алгоритмов, вы не сказали. Как с его помощью деньги то заработать и прибыль бизнесу принести?
                                                Вот я тупой прогер — создаю формочки и клиенты всякие для заказчиков, во всякие базы данных лазию и инфу оттуда гоняют туда — обратно. Так вот, я правильно понимаю, что для моего профилирования F# мне не помошник?

                                                Ну ладно, алгоритмы. Возьмем цифровую обработку сигналов. Из своего опыта я вам скажу, что для промышленной реализации кодека изображений, на основе кодирования коэффициентов дискретного-вейвлет преобразования, например, да даже простого джипега, F# не подойдет (пусть даже кода будет меньше), ибо плюсы в данном случае быстрее.

                                                Может, студентов на лабах учить алгоритмы программировать? Вряд ли они променяют на F# любимый matlab.
                                                  0
                                                  вопрос конечно фундаментальный, и в двух словах не ответить.
                                                  На самом деле у ФП в целом и F# много преимуществ перед императивными языками, например простота в распараллеливании вычислений, вытекающая из самой идеологии (что кстати очень актуально в связи с хз сколькими ядрами на проце, которые толком и не использует-то никто), обработка больших объемов данных кстати, из бд, тоже будет писаться проще и выглядеть красивее. Linq то глядите как приглянулся всем (ну многим по крайней мере), а ведь по сути — это и есть шажок к ФП. Ну с другой стороны конечно интерфейсы пока явно не стезя F#. =)
                                                  Насчет быстродействия — конечно плюсам он на одном ядре проигрывает, а уже C# — не так чтобы очень сильно (голословно, пруфлинка нету). С другой стороны если взять в расчет распараллеливание, то уже могут быть варианты.
                                                    0
                                                    Ага. Вот вы статью хорошую написали, эдакий «живой» справочник, но что с ней делать обычному рабочему люду и как ее использовать — непонятно. Все хорошо разжевано, но как интегрировать решения на F# в существующие подходы — тоже неясно — надо идти копать в мсдн. Вообще, имхо, самая первая, нулевая проблема всех новых фишек и рюшек, аля нового ФЯП от Microsoft в том, что изначально мало кто соображает, как его применять.

                                                    Статья была бы еще более ценнее если бы она была ну хоть капельку похожа на how-to подход. Например — у нас вот у всех есть вот такого рода решения задачи, изначально (через шарп, к примеру), она решается вот так. А теперь у нас есть F# и решить эту задачу можно вот так-то. Смотрите, какие бонусы и плюсы мы получаем, теперь это работает быстрее, глюков меньше, проблемы решаются такие-то и такие-то.

                                                    Вот каким образом мне, .net девелоперу использовать в своих задачах F#? Я не знаю, мне надо идти ресерчить это дело, я потрачу свое время, чтобы разрюхать, как мне подключить к решению F#. И только после этого я возьмусь за вашу статью, а без этого — нет смысла.

                                                    Во всех статьях на хабре, что проскальзывали по ф-шарпу, нет точки входа к возможности воспользоваться предложенными знаниями. Ну что мне толку с алгоритма определения последовательностей чисел Фибоначчи? Никакого, поверьте:)

                                                    А вот если бы была разжевана актуальная задача, ну например более удобная реорганизация большого (огромного) массива данных в структуры данных удобные для построения отчетов — этож цены такой статье бы не было! У нас вот восьмиядерник каждую ночь перемалывает стастистику по вводу и проработке нормативных правовых актов по всей РФ, так мы вынуждены весь процессинг в хранимых процедурах вести, по другому квалификации не хватает и времени, чтобы разобраться. Ну это я так, к слову о наболевшем:)
                                                      0
                                                      я в следующей статье собираюсь выложить код небольшой утилиты на F#, которую лично написал, и которая действительно была мне полезна по работе. Она конечно не бог весть что, но явно не числа Фибоначчи (которые кстати везде где я видел на хабре написаны неправильно, потому что не учитывают возможный StackOverflow :)).
                                                      Думаю дня через три-четыре будет, а то я еще от написания этой не отошел. Можете заглянуть.
                                                        0
                                                        >Я не знаю, мне надо идти ресерчить это дело

                                                        Смотреть видеоролики не так напряжно, как идти чего-то ресерчить. Начни с презентации на PDC 2008, 300 Мб: channel9.msdn.com/pdc2008/TL11/

                                                        >А теперь у нас есть F# и решить эту задачу можно вот так-то.

                                                        В этом докладе есть подобные наглядные примеры.
                                                      0
                                                      Ингода, просто доставляет удовольствие «помучить» новые языки программирования. По F# могу посоветовать книгу самого Дона Сайма. Мне у него понравились примеры использования F# при программировании на ASP.NET — очень эффективно с различными типами данных можно работать. Для рисования формочек F# наврядли подойдет, да MS и не позиционирует его с этой стороны. По поводу библиотек, то я бы лично не рискнул на F# их писать, пока сыроват F# для этих дел.
                                                      Если касаться вопросов ЦОС, то здесь лучше использовать спциализированную аппаратуру или мощнсти современных GPU, управляемый код здесь совсем не нужен.
                                                        0
                                                        Дык вот ктоб на хабр подобный подход запостил (F# + ASP.NET)? :)
                                                        0
                                                        Променяем ;-) Хотя бы в общеобразовательных целях. Да, о применении много вопросов, но с другой стороны, выходит, что все спрашивают, а применять никто почти и не пытается. Мне приходилось сталкиваться при поиске работы по ключевому слову F# (просто ради интереса), с какой-то финансовой компанией, где требовался аналитик со знанием F#. Я думаю, что стоит говорить об успешности его применения после широкого внедрения Microsoft Visual Studio 2010.

                                                        А пока для примера хотелось бы продемонстрировать код метода градиентного спуска (курс: численные методы оптимизации) и сделать пару комментариев.

                                                          0
                                                          Променяем ;-) Хотя бы в общеобразовательных целях. Да, о применении много вопросов, но с другой стороны, выходит, что все спрашивают, а применять никто почти и не пытается. Мне приходилось сталкиваться при поиске работы по ключевому слову F# (просто ради интереса), с какой-то финансовой компанией, где требовался аналитик со знанием F#. Я думаю, что стоит говорить об успешности его применения после широкого внедрения Microsoft Visual Studio 2010.

                                                          А пока для примера хотелось бы продемонстрировать код метода градиентного спуска (курс: численные методы оптимизации) и сделать пару комментариев.

                                                          0
                                                          #light

                                                          (* Один шаг метода градиентного спуска *)
                                                          (* points — некоторая начальная точка в n-мерном пространстве
                                                          derivates — список частных производных по каждой из координат точки.
                                                          Подразумевается, что размерности points и derivates совпадают
                                                          rezFunction — целевая функция *)
                                                          let GradientDescent point derivates rezFunction =
                                                          (* Начальный шаг *)
                                                          let eta = 1.0
                                                          (* Конечный шаг *)
                                                          let eps = 1.0e-15
                                                          let grad = derivates |> List.map (fun f -> f point)
                                                          let currentValue = rezFunction point
                                                          let rec gradStep currentPoint etaCurrent =
                                                          if etaCurrent < eps then
                                                          currentPoint
                                                          else
                                                          let newPoint = List.map2 (fun pointCoordinate gradCoordinate -> pointCoordinate — etaCurrent * gradCoordinate) currentPoint grad
                                                          let newValue = rezFunction newPoint
                                                          if newValue < currentValue then
                                                          newPoint
                                                          else
                                                          gradStep currentPoint (0.5 * etaCurrent)
                                                          gradStep point eta

                                                          Преимущество подхода, на мой взгляд в том, что функция не зависит от передаваемых ей частных производных. Конечно, это можно написать и на C/C++/C#, но на этих языках это выглядит несколько неественно, а в этом случае, при создании списка функций-«частных переменных» используется каррирование, что делает в этом случае язык F# крайне удобным инструментом.
                                                            –1
                                                            > применять его выше по коду, чем место его определения, нельзя

                                                            а если очень хочется? например, когда две функции вызывают друг друга.

                                                            > не получив полного набора параметров, она попросту возвращает другую функцию

                                                            то есть, если я забыл один параметр, то вместо сообщения об ошибке я получую чёрт знает что?

                                                            > Списки задаются в прямоугольных скобках [ ], последовательности — в {}, массивы — в [| |].

                                                            зачем стока? в чём их отличия?
                                                              0
                                                              1. Не знаю, когда такая способность может пригодиться, но если очень нужно, тогда можно сделать методами класса — с ними все как обычно.
                                                              2. Ну почему черт знает что — другую функцию. Ну в конце концов, вы же ее тоже где-то будете использовать? Там и получите вашу ошибку, о том, что пытаетесь подставить значение неверного типа (ну например требуется int, а вы ставите int -> int). Ну честно говоря не разу еще в такую ситуацию не попадал.
                                                              3. Чем-то да отличаются ) Списки имеют тип, который я выше показал, их можно легко разбирать pattern matching'ом, массивы являются изменяемым типом, их элементы можно изменять по отдельности. Последовательности — это просто здешний мем для IEnumerable, то есть ею может быть все что угодно. А еще они могут быть бесконечными =)
                                                                –1
                                                                1. то есть к классу можно обращаться независимо от того где он определён? ну и какой подход тогда более декларативен? ;-)

                                                                2. трабла в том, что я получу ошибку не там, где она на самом деле есть. и придётся бегать с отладчиком в поисках места, где же вместо инта появляется функция.

                                                                  0
                                                                  не туда откоментил, сорри.
                                                              0
                                                              1. я этого не говорил. внутри класса можно обращаться как хочется:
                                                              type Sample = class
                                                              new () = {}
                                                              member x.A i = x.B i
                                                              member x.B i = x.A i
                                                              end
                                                              а к самому классу можно обращаться только ниже, как в пределах файла, так и в пределах проекта (порядок файлов имеет значение) Из другого проекта — пожалуйста, реф поставить и вперед.

                                                              2. Ну чем меньше возможностей, тем меньше проблем, как бы. :) Но вообще я думаю вряд ли возможно увести это неверное значение дальше чем на один шаг.
                                                                0
                                                                В принципе все функциональные языки делаются под ряд определенных задач, потому они удобны. А синтаксис кстати не такой уж и сложный, по крайней мере не вызывает отвращения и страха.
                                                                • НЛО прилетело и опубликовало эту надпись здесь

                                                                  Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                                                  Самое читаемое