[Перевод] Функциональное мышление. Часть 8
Привет, Хабр! Мы с небольшим запозданием возвращаемся с новогодних каникул с продолжением нашей серии статей про функциональное программирование. Сегодня расскажем про понимание функций через сигнатуры и определение собственных типов для сигнатур функций. Подробности под катом!
Не очевидно, но в 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 за подготовку данной статьи к публикации.