Scala и функциональный стиль: практический пример, автор Венкат Субраманиам

Отрывок из книги «Функциональное программирование: антология PragPub»

✒ Примечание редактора. На Pragmatic Bookshelf представлен широкий выбор книг по тематике функционального программирования. Вы можете начать с прочтения Функциональное программирование: антология PragPub прямо на Medium или просмотреть некоторые из наших других предложений.

Функциональное программирование делает упор на неизменяемость, но в равной степени речь идет о проектировании с преобразованием состояния и композицией функций.

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

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

Начнем с примерного списка символов тикера.

​ val tickers = List("AAPL", "AMD", "CSCO", "GOOG", "HPQ", "INTC",
​     "MSFT", "ORCL", "QCOM", "XRX")

Для удобства (и во избежание загадочных символов в коде) давайте создадим класс case для представления акций и их цены (классы case полезны для создания неизменяемых экземпляров в Scala, которые предоставляют довольно много преимуществ, особенно простота сопоставления с образцом, общий функциональный стиль, который вы подробно изучите в Главе 10, Шаблоны и преобразования в Эликсире).

​ case class StockPrice(ticker : String, price : Double) {
​   def print =
​         println("Top stock is " + ticker + " at price $" + price)
​ }

Учитывая символ тикера, мы хотим получить последнюю цену акции для этого символа. К счастью, Yahoo упрощает эту задачу.

​ def getPrice(ticker : String) = {
​   val url = s"http://download.finance.yahoo.com/d/quotes.csv?s=${ticker}&f=snbaopl1"
​   val data = io.Source.fromURL(url).mkString
​   val price = data.split(",")(4).toDouble
​   StockPrice(ticker, price)
​ }

Мы извлекаем последнюю цену акции из URL-адреса Yahoo, анализируем результат и возвращаем экземпляр StockPrice с символом тикера и значением цены.

Чтобы помочь нам выбрать самые дорогие акции стоимостью не более 500 долларов, нам нужны две функции: одна для сравнения цен двух акций, а другая — для определения того, не превышает ли данная цена акций 500 долларов.

​ def pickHighPriced(stockPrice1 : StockPrice, stockPrice2 :
​         StockPrice) =
​   if(stockPrice1.price > stockPrice2.price) stockPrice1
​         else stockPrice2
​ 
​ def isNotOver500(stockPrice : StockPrice) = stockPrice.price < 500

Учитывая два экземпляра StockPrice, функция pickHighPriced возвращает более высокую цену. isNotOver500 вернет true, если цена меньше или равна $500, false в противном случае.

Вот как мы подошли бы к проблеме в императивном стиле:

​ import scala.collection.mutable.ArrayBuffer
​ 
​ val stockPrices = new ArrayBuffer[StockPrice]
​ for(ticker <- tickers) {
​   stockPrices += getPrice(ticker)
​ }
​ 
​ val stockPricesLessThan500 = new ArrayBuffer[StockPrice]
​ for(stockPrice <- stockPrices) {
​   if(isNotOver500(stockPrice)) stockPricesLessThan500 += stockPrice
​ }
​ 
​ var highestPricedStock = StockPrice("", 0.0)
​ 
​ for(stockPrice <- stockPricesLessThan500) {
​   highestPricedStock =
​         pickHighPriced(highestPricedStock, stockPrice)
​ }
​ 
​ highestPricedStock print
​ //Top stock is AAPL at price $377.41

Давайте пройдемся по коду, чтобы увидеть, что мы сделали.

Сначала мы создаем экземпляр ArrayBuffer, который является изменяемой коллекцией. Мы вызываем функцию getPrice() для каждого тикера и заполняем stockPrices ArrayBuffer экземплярами StockPrice.

Во-вторых, мы перебираем эти цены акций и выбираем только акции стоимостью менее 500 долларов и добавляем к stockPricesLessThan500 ArrayBuffer. Это приводит к возможно меньшему количеству элементов, чем мы начали.

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

Мы можем улучшить этот код, использовать несколько коллекций, если хотим, обернуть код в отдельные функции и поместить их в класс, если захотим. Однако это не повлияет на основной подход, который мы выбрали: императивный стиль с изменяемой структурой данных. Состояние коллекции акций и их цены претерпели немало мутаций.

Теперь давайте напишем этот код в функциональном стиле. Готовый?

​ tickers map getPrice filter isNotOver500 reduce pickHighPriced print

Были сделаны. Вот и все, достаточно маленькое, чтобы поместиться в твит. Хорошо, к этой лаконичности нужно привыкнуть. Давайте пройдемся по нему.

tickers map getPrice сначала преобразует коллекцию тикеров в коллекцию StockPrice экземпляров. Для каждого символа тикера теперь у нас есть имя и цена в этой коллекции. Затем функция фильтра применяет isNotOver500 к этой коллекции и преобразует ее в меньшую коллекцию StockPrices только с акциями, цена которых не превышает 500 долларов. Функция reduce идет дальше, чтобы выбрать самые дорогие акции, которые мы, наконец, передаем методу print из StockPrice.

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

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

Надеемся, вам понравился этот отрывок. Вы можете продолжить чтение Функциональное программирование: Антология PragPub прямо на Medium:



Или купите электронную книгу прямо на The Pragmatic Bookshelf:



Чтобы получить печатную копию, посетите bookshop.org.