Уважаемые пользователи Голос!
Сайт доступен в режиме «чтение» до сентября 2020 года. Операции с токенами Golos, Cyber можно проводить, используя альтернативные клиенты или через эксплорер Cyberway. Подробности здесь: https://golos.io/@goloscore/operacii-s-tokenami-golos-cyber-1594822432061
С уважением, команда “Голос”
GOLOS
RU
EN
UA
ninjas
7 лет назад

Академия: Введение в функциональное программирование. Конспект четвертой недели курса

Всем привет. Я принимаю участие в проекте Академия от @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, охранные выражения и сопоставления с образцом. Также мы разобрали лямбда-выражения, которые могут сильно облегчить жизнь и помогают хорошо иллюстрировать каррирование функций

Анонс следующей части

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

0
274.437 GOLOS
На Golos с May 2017
Комментарии (2)
Сортировать по:
Сначала старые