Привет, Хабр! Мы с небольшим запозданием возвращаемся с новогодних каникул с продолжением нашей серии статей про функциональное программирование. Сегодня расскажем про понимание функций через сигнатуры и определение собственных типов для сигнатур функций. Подробности под катом!




Не очевидно, но в F# два синтаксиса: для обычных (значимых) выражений и для определения типов. Например:


[1;2;3]      // обычное выражение
int list     // выражение типов

Some 1       // обычное выражение
int option   // выражение типов

(1,"a")      // обычное выражение
int * string // выражение типов

Выражения для типов имеют особый синтаксис, который отличается от синтаксиса обычных выражений. Вы могли заметить множество примеров этого синтаксиса во время работы с FSI (FSharp Interactive), т.к. типы каждого выражения выводятся вместе с результатами его выполнения.


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


Вот несколько примеров сигнатур с синтаксисом типов:


// синтаксис выражений        // синтаксис типов
let add1 x = x + 1            // int -> int
let add x y = x + y           // int -> int -> int
let print x = printf "%A" x   // 'a -> unit
System.Console.ReadLine       // unit -> string
List.sum                      // 'a list -> 'a
List.filter                   // ('a -> bool) -> 'a list -> 'a list
List.map                      // ('a -> 'b) -> 'a list -> 'b list

Понимание функций через сигнатуры


Часто, даже просто изучив сигнатуру функции, можно получить некоторое представление о том, что она делает. Рассмотрим несколько примеров и проанализируем их по очереди.


int -> int -> int

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


int -> unit

Данная функция принимает int и возвращает unit, что означает, что функция делает что-то важное в виде side-эффекта. Т.к. она не возвращает полезного значения, side-эффект скорее всего производит операции записи в IO, такие как логирование, запись в базу данных или что-нибудь похожее.


unit -> string

Эта функция ничего не принимает, но возвращает string, что может означать, что функция получает строку из воздуха. Поскольку нет явного ввода, функция вероятно делает что-то с чтением (скажем из файла) или генерацией (например случайной строки).


int -> (unit -> string)

Эта функция принимает int и возвращает другую функцию, которая при вызове вернет строку. Опять же, вероятно, функция производит операцию чтения или генерации. Ввод, скорее всего, каким-то образом инициализирует возвращаемую функцию. Например, ввод может быть идентификатором файла, а возвращаемая функция похожа на readline(). Или же ввод может быть начальным значением для генератора случайных строк. Мы не можем сказать точно, но какие-то выводы можем сделать.


'a list -> 'a

Функция принимает список любого типа, но возвращает лишь одно значение этого типа. Это может говорить о том, что функция агрегирует список или выбирает один из его элементов. Подобную сигнатуру имеют List.sum, List.max, List.head и т.д.


('a -> bool) -> 'a list -> 'a list

Эта функция принимает два параметра: первый — функция, преобразующая что-либо в bool (предикат), второй — список. Возвращаемое значение является списком того же типа. Предикаты используют для того, чтобы определить, соответствует ли объект некому критерию, и похоже ли, что данная функция выбирает элементы из списка согласно предикату — истина или ложь. После этого она возвращает подмножество исходного списка. Примером функции с такой сигнатурой является List.filter.


('a -> 'b) -> 'a list -> 'b list

Функция принимает два параметра: преобразование из типа 'a в тип 'b и список типа 'a. Возвращаемое значение является списком типа 'b. Разумно предположить, что функция берет каждый элемент из списка 'a, и преобразует его в 'b, используя переданную в качестве первого параметра функцию, после чего возвращает список 'b. И действительно, List.map является прообразом функции с такой сигнатурой.


Поиск библиотечных методов при помощи сигнатур


Сигнатуры функций очень важны в поиске библиотечных функций. Библиотеки F# содержат сотни функций, что поначалу может сбивать с толку. В отличие от объектно-ориентированных языков, вы не можете просто "войти в объект" через точку, чтобы найти все связанные методы. Но если вы знаете сигнатуру желаемой функции, вы быстро сможете сузить круг поисков.


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


'a list -> 'a list -> 'a list

Теперь перейдем на сайт документации MSDN для модуля List, и поищем похожую функцию. Оказывается, существует лишь одна функция с такой сигнатурой:


append : 'T list -> 'T list -> 'T list

То что нужно!


Определение собственных типов для сигнатур функций


Когда-нибудь вы захотите определить свои собственные типы для желаемой функции. Это можно сделать при помощи ключевого слова "type":


type Adder = int -> int
type AdderGenerator = int -> Adder

В дальнейшем вы можете использовать эти типы для ограничения значений параметров функций.


Например, второе объявление из-за наложенного ограничения упадет с ошибкой приведения типов. Если мы его уберём (как в третьем объявлении), ошибка исчезнет.


let a:AdderGenerator = fun x -> (fun y -> x + y)
let b:AdderGenerator = fun (x:float) -> (fun y -> x + y)
let c                = fun (x:float) -> (fun y -> x + y)

Проверка понимания сигнатур функций


Хорошо ли вы понимаете сигнатуры функций? Проверьте себя, сможете ли вы создать простые функции с сигнатурами ниже. Избегайте явного указания типов!


val testA = int -> int
val testB = int -> int -> int
val testC = int -> (int -> int)
val testD = (int -> int) -> int
val testE = int -> int -> int -> int
val testF = (int -> int) -> (int -> int)
val testG = int -> (int -> int) -> int
val testH = (int -> int -> int) -> int

Дополнительные ресурсы


Для F# существует множество самоучителей, включая материалы для тех, кто пришел с опытом C# или Java. Следующие ссылки могут быть полезными по мере того, как вы будете глубже изучать F#:



Также описаны еще несколько способов, как начать изучение F#.


И наконец, сообщество F# очень дружелюбно к начинающим. Есть очень активный чат в Slack, поддерживаемый F# Software Foundation, с комнатами для начинающих, к которым вы можете свободно присоединиться. Мы настоятельно рекомендуем вам это сделать!


Не забудьте посетить сайт русскоязычного сообщества F#! Если у вас возникнут вопросы по изучению языка, мы будем рады обсудить их в чатах:



Об авторах перевода


Автор перевода @kleidemos
Перевод и редакторские правки сделаны усилиями русскоязычного сообщества F#-разработчиков. Мы также благодарим @schvepsss и @shwars за подготовку данной статьи к публикации.

Комментарии (6)


  1. build_your_web
    07.02.2019 21:36
    -1

    Думаю, выскажу непопулярное на Хабре мнение, но всё же…
    F# хорош как функциональный язык, но плохо годится для написания в императивном стиле. Через строчку писать |> ignore — это не то, что хотелось бы от современного языка. И как бы в статьях не хвалили чисто функциональный подход, f# не является удобным универсальным языком(функциональным+императивным).


    К тому же, разработчики языка решили, что просто сменить язык — это недостаточно круто, и поэтому они отрешились от привычного для c# разработчиков linq.


    Ещё авторы языка решили, что привычные для c# разработчиков Task — это нечто чуждое для такого прекрасного функционального языка, поэтому все вызовы к асинхронным функциями из .Net фреймворка должны сопровождаться |> Async.AwaitTask вместо привычного лаконичного ключевого слова await.


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


    1. Szer
      08.02.2019 02:09
      +1

      Через строчку писать |> ignore — это не то, что хотелось бы от современного языка.

      Можете выключить этот варнинг навсегда через:
      #nowarn "0020"
      и говнокодить как на C# сколько влезет.


      разработчики языка решили, что просто сменить язык — это недостаточно круто, и поэтому они отрешились от привычного для c# разработчиков linq.

      open System.Linq
      
      [1..10].Select(fun x -> x + 1)
             .Where(fun x -> x % 2 = 0)
             .ToArray()

      Но если что, то Дон Сайм (автор языка), является мейнтейнером либы FSharp.Core.Fluent.
      Она даёт доступ к функциям "через точку" для List, Array, Array2D, Array3D, Seq, Event и Observable. Так что там ещё авторы языка сделали плохого?)


      Ну и конечно же, LINQ появился позже чем этот синтаксис:


      [1..10]
      |> Seq.map (fun x -> x + 1)
      |> Seq.filter (fun x -> x % 2 = 0)

      Поэтому "отрешиться" от LINQ авторы F# не могли в принципе.


      Ещё авторы языка решили, что привычные для c# разработчиков Task — это нечто чуждое для такого прекрасного функционального языка, поэтому все вызовы к асинхронным функциями из .Net фреймворка должны сопровождаться |> Async.AwaitTask вместо привычного лаконичного ключевого слова await.

      Авторы языка запилили async/await в F# за 2 года до появления async/await в C# :)
      Поэтому и Async<T>, и асинхронные методы в стримах и HttpRequestMessage появились сильно раньше. Авторы языка C# решили что они хотят свой Async<T> и назвали его Task<T>.


      image


      Вы постоянно путаете причину и следствие.
      Почти все фичи в начале появлялись в F#, поэтому авторы языка просто не могли скопировать дизайн из C#. Дизайна просто не было!


      1. build_your_web
        08.02.2019 09:54

        Я смотрю на F# глазами разработчика, который хочет перейти на него с C#.
        В моем понимании удобство использования .Net Framework не менее важно, что фичи языка.
        Поэтому если в .Net FW решили внедрить TPL (Task — это фича фреймворка, а не языка), то это значит что нужно привести синтаксис языка под эту новую функциональность, как это сделали в C#, а не говорить, что мы это придумали раньше, поэтому мы теперь меняться не будем (пусть весь остальной мир перепишет свои либы под Async<T>, вместо Task<T>).


        1. Szer
          08.02.2019 11:03

          Поэтому если в .Net FW решили внедрить TPL (Task — это фича фреймворка, а не языка), то это значит что нужно привести синтаксис языка под эту новую функциональность, как это сделали в C#

          Поддержку Task конечно же ввели сразу же, добавив Async.AwaitTask и Async.StartAsTask. Я вот лично не видел чтобы авторы C# вводили интероп с Async :)

          Если пользоваться этими методами неудобно (можно понять), то вам никто не мешает расширить базовый AsyncBuilder для работы с тасками:
          image


          Или взять taskBuilder, который используется в Giraffe (кстати второй после Zebra Fullstack фреймворк на дотнете по скорости)


  1. Szer
    08.02.2019 02:07

    del


  1. Neftedollar
    09.02.2019 01:53
    +1

    Полезная статья.
    Всегда когда хочу найти нужную ф-цию ищу её по сигнатуре. Не проходите мимо.