Академия: Введение в функциональное программирование. Конспект четвертой недели курса
Всем привет. Я принимаю участие в проекте Академия от @ontofractal и сегодня публикую очередную часть конспекта курса Введение в функциональное программирование от Delft University of Technology. В прошлых частях мы:
- узнали историю ФП и его основные преимущества
- познакомились с некоторыми функциями из встроенной библиотеки Prelude
- выяснили, как работает система типов Хаскеля и в чем заключаются ее особенности; а также научились работать с несколькими важнейшими функциональными техниками: каррированием, перегрузкой и полиморными функциями
В этой части мы будем разбираться с разными способами определения функций: при помощи условных и охранных выражений, сопоставления с образцом и лямбда-выражений
Условные выражения
Одна из самых необходимых в программировании вещей - это возможность разветвлять выполнение кода в зависимости от условий, поэтому во всех популярных языках есть условные выражения, позволяющие направлять ход событий по тому или иному сценарию
Хаскель не является исключением, и один из способов создать несколько сценариев выполнения функции - это использование операторов if
и else
firstCond height =
if height < 150
then 'You're too low'
else if height > 200
then 'You're too high'
else 'You have normal height'
Здесь мы наглядно видим, что условные выражения в Хаскеле не особо отличаются от таковых в императивных языках. Однако, одно незаметное, но важное отличие. Во-первых, в Хаскеле конструкция if..else
является выражением, а не утверждением. А так как вычисление выражения не может просто прерваться на полуслове, в условном выражении всегда должен присутствовать блок else
. Еще одна особенность - это стиль написания многострочных условных выражений. Правила хорошего тона предполагают, что очередной блок else
выравнивается по предыдущему then
Охранные выражения
Блоки if..else
хороши, когда у нас есть всего несколько вариантов развития событий. Но представьте, что нам нужно придумать 6 разных сценарий поведения функции. Полученный код будет очень громозким и трудным для восприятия. Как раз для таких случаев существуют охранные выражения, имеющие следующий синтаксис:
guardians :: (Num a, Ord a) => a -> [Char]
guardians height
| height < 150 = "You're too low"
| height < 170 = "Can be better"
| height < 180 = "Not bad"
| height < 200 = "You're so tall"
| otherwise = "Calm down tough guy!"
Намного удобнее, чем написать 3 блока else if
, не правда ли? Отметим также важный момент: между параметрами функции и охранным выражениям не ставится знак =
Сопоставление с образцом
Еще один способ ветвления потока выполнения функции - это использование сопоставления с образцом. Помните нашу функцию customProduct
? Мы определили ее следующим образом:
customProduct [] = 0
customProduct xs = foldl (*) 1 xs
Такой способ описания функции и называется сопоставлением с образцом. Суть в том, чтобы предоставить функции несколько вариантов того, как могут выглядеть входные параметры. В нашем случае таких вариантов два: пустой и непустой списки. В принципе же количество вариантов ничем не ограничено
Последнее, о чем стоит упомянуть в связи с контрольными структурами - это то, что наиболее общие варианты нужно располагать в конце выражения. Например, если мы переопределим функцию &&
следующим образом:
customAnd :: Bool -> Bool -> Bool
customAnd True True = True
customAnd _ _ = False
то все будет работать нормально. Однако, если поменять варианты местами:
customAnd :: Bool -> Bool -> Bool
customAnd _ _ = False
customAnd True True = True
то функция всегда будет выдавать False
, ведь под условие _ && _
может подпадать все что угодно, включая True && True
Результат выполнения "сломанной" версии customAnd
. Заметьте, что GHCi предупредил нас о возможных проблемах еще в момент компиляции
На этом я предлагаю закончить с контрольными структурами и перейти к следующей теме - лямбда-выражениям
Лямбда-выражения
Несмотря на то, что их название может показаться необычным, в сущности, лямбда-выражения - это просто знакомые всем анонимные функции. Они создаются с помощью оператора \
и обычно используются в двух случаях
Параметр функции высшего порядка
Одна из ключевых особенностей ФП - это широкое использование композиции функций. Даже за то короткое время, в течение которого мы изучаем премудрости ФП, мы уже не раз успели столкнуться с подобными примерами. Например, в нашей реализации стандартной функции product
мы использовали другую функцию foldl
, которая принимает определенную функцию и комбинирует с ее помощью стартовое значение с элементом списка. Другой пример - это функция . С ее помощью мы может преобразить каждый элемент списка, применяя к нему заданную функцию. Например, давайте напишем функцию, принимающую список и возводящую каждый его элемент в квадрат:
square :: Num b => [b] -> [b]
square xs = map f xs
where f x = x ^ 2
При помощи лямбда-функций мы можем сделать функцию менее громозкой:
square xs = map (\x -> x ^ 2) xs
И, кстати, обратите внимание на тип функции. Узнаете? Да, здесь мы воспользовались перегрузкой функций, ограничив набор ее возможных аргументов лишь теми, что относятся к классу типов Num
Наглядное каррирование
Помимо упрощения синтаксиса функций, лямбда-выражения играют другую важную роль. Благодаря их использованию можно гораздо более явно указать, что в наша функция каррирована
Если помните, в предыдущей части я упоминал, что когда мы используем следующую запись функции с несколькими параметрами:
sumTwo :: Int -> Int -> Int
sumTwo a b = a + b
то мы просто скрываем реальное положение под слоем встроенного в Хаскель синтаксического сахара. На самом же деле, функция sumTwo
возвращает не значение a + b
, а другую функцию. Для наглядности я демонстрировал это с помощью примера на JS:
function sumTwo(a, b) = {
return function(b) {
return a + b
}
}
Теперь же мы можем написать подобный пример и на Хаскеле, используя лямбда-выражения:
sumTwo a = \b -> a + b
Причем, подобный стиль можно использовать не только с учебными целями, но и в реальных программах, чтобы люди, читающие ваш код, могли быстрее понять, что функция использует каррирование
Сечения
В прошлой части мы говорили о том, что каррирование позволяет нам воспользоваться крайне удобной техникой, называемой частичное применение. Ее суть в том, чтобы связать с переменной функцию, которой мы передаем неполное число параметров. Тогда мы демонстрировали это на примере функций, применяемых с помощью стандартной префиксной нотации. Однако, такая техника применима и для инфиксных функций - нужно просто предоставить им параметр только с одной стороны. Например, если мы часто пользуемся функцией, увеличивающей значение параметра на 1, мы можем записать ее так:
increaseByOne = (+1)
После чего можно будет легко применять ее к каким угодно числам
Что показалось наиболее важным в этой части?
В этой части мы познакомились с несколькими одной из наиболее часто используемых техник в программировании: условными выражениями. Мы узнали, как строить блоки if..else
, охранные выражения и сопоставления с образцом. Также мы разобрали лямбда-выражения, которые могут сильно облегчить жизнь и помогают хорошо иллюстрировать каррирование функций
Анонс следующей части
В конспекте пятой недели мы продолжим знакомство со списками и узнаем, как пользоваться очень мощным инструментов для их создания - генераторами списков