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.