Тесты в Clojure (второй фрагмент)

Продолжение первой части

Содержание

Транзакция с откатом

Другой способ избавиться от изменений в базе — обернуть все действия с ней в особую транзакцию. Такая транзакция завершается оператором не COMMIT, а ROLLBACK, что значит откатить все команды. С точки зрения базы это выглядит так:

BEGIN;
INSERT INTO users ...
INSERT INTO profiles ...
UPDATE users SET name=...
ROLLBACK;

При выходе из транзакции мы не увидим последствий INSERT, UPDATE и других команд, выполненных в ее рамках.

В пакет JDBC входит функция db-set-rollback-only!. Она принимает транзакционное соединение и выставляет ему флаг rollback. Если флаг установлен, JDBC завершает блок транзакции оператором ROLLBACK.

Вы уже знакомы с макросом with-db-transaction: внутри его тела действует транзакционное соединение, которые получают из JDBC-спеки. Напишем свой макрос with-db-transaction, который делает то же самое, но дополнительно устанавливает флаг отката:

(defmacro with-db-transaction
  [[t-conn & bindings] & body]
  `(jdbc/with-db-transaction [~t-conn ~@bindings]
     (jdbc/db-set-rollback-only! ~t-conn)
     ~@body))

Пример его работы:

(with-db-rollback [tx *db*]
  (println "Inserting the data...")
  (jdbc/insert! tx :users {:name "Ivan"})
  (let [...]
    (do-something)))

Разработчик следит за тем, чтобы все действия — вставка данных, логика теста — протекали через tx, а не *db*. Иначе изменения в рамках обычного соединения останутся в базе. Прямо сейчас загрузчик load-data данных ссылается на глобальную переменную *db*. Чтобы сообщить ему транзакционное соединение, придется либо передать параметр, либо связать *db* с tx формой binding.

Пример с параметром: все действия с базой принимают tx, который мы установили на вершине теста.

(deftest test-user-with-rollback
  (with-db-rollback [tx *db*]
    (load-data tx)
    (let [user (get-user-by-name tx "Ivan")]
      (is (= "Ivan" (:name user))))))

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

(deftest test-user-with-rollback
  (with-db-rollback [tx *db*]
    (binding [*db* tx]
      (load-data)
      (let [user (get-user-by-name "Ivan")]
        (is (= "Ivan" (:name user)))))))

Реализация зависит от того, как в проекте устроена работа с базой. Решение с откатом хорошо подходит для mount и похожей архитектуры, когда база это глобальная переменная. В случае с системой компонентов могут возникнуть трудности с передачей tx от теста к логике и наоборот.

В свободное время подумайте, как оформить фикстуру с макросом with-db-rollback. Будет ли она работать с системой компонентов? Что необходимо в этом случае?

Тестирование веб-приложений

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

В главе про веб-разработку мы пришли к важному выводу. Веб-приложение на каждом уровне остается функций одного аргумента. Где-то внизу это обработчик конкретной страницы, условный (view-profile-page [request]). Комбинация маршрутов и middleware не меняет это соглашение. Конечный объект app, много раз обернутый в middleware, по-прежнему принимает словарь запроса и возвращает словарь ответа.

Это определяет, как писать тесты для приложения. Типичный тест составляет запрос и вызывает приложение как функцию. В ответе ищут HTTP-статус и проверяют на успех (200, 201) или неудачу (404, 403). Если это JSON-ответ, его тело считывают в коллекцию и сравнивают с образцом.

Вспомним приложение из первой главы. Отдельные страницы мы соединили в маршруты с помощью Compojure. Получилось “голое” приложение. Так мы назвали его потому, что оно многого не умеет. Например, читать параметры запроса, работать с JSON, куками, сессиями и так далее. Приложение узнает все это из middleware, в которые мы его оборачиваем.

(defroutes app-naked
  (GET "/"      request (page-index request))
  (GET "/hello" request (page-hello request))
  page-404)

(def app
  (-> app-naked
      wrap-session
      wrap-keyword-params
      wrap-params
      wrap-json-body
      wrap-json-response))

Напишем несколько тестов для этого приложения. Пусть это будет главная страница и любая другая, которой нет в дереве маршрутов. Для экономии места проверим только статус ответа.

(deftest test-app-index
  (let [request {:request-method :get :uri "/"}
        response (app request)
        {:keys [status body]} response]
    (is (= 200 status))))

(deftest test-app-page-not-found
  (let [request {:request-method :get :uri "/missing"}
        response (app request)
        {:keys [status body]} response]
    (is (= 404 status))))

Как видно из примеров, писать тесты для веб-приложения нетрудно. Однако, мы не бросим читателя на этом месте. Перечислим несколько приемов, которые облегчат работу с тестами.

Приложение целиком. Избегайте ситуации, когда тест вызывает не приложение, а один из обработчиков. Плохой пример: (page-index request) вместо app. На текущем уровне вызов конкретной функции ничего не дает. Даже если страница работает по отдельности, нет гарантии, что запрос пройдет сквозь маршруты и все middleware. В боевых проектах middleware несут весомую логику. Это права доступа, разбор токенов и JWT, данные из прошлых запросов в сессии. Убрав все это из теста, вы тем самым обманываете себя. Объект app, который вы тестируете, должен быть максимально “заряжен”, т.е. близок к настоящему веб-серверу.

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

/users/?page=2&order=name&name_contains=smith&search_type=relevance

, что совершенно нечитаемо и тяжело в поддержке. В словаре запроса легко перепутать ключи :request-method и :method, что автор и сделал при написании книги.

Чтобы избежать подобных ошибок, воспользуйтесь ring-mock — библиотекой для запросов к Ring-приложениям. Ее функции покрывают основные сценарии в тестах. Так, функция request принимает метод и путь. Если добавить словарь параметров, то для GET они станут строкой запроса, а для POST — его телом. URL-кодировку библиотека берет на себя. Другая функция json-body пишет в запрос тело из коллекции. Рассмотрим несколько примеров.

Простая страница, запрос GET /help:

(mock/request :get "/help")

Ввод данных в форму поиска, GET /movies?search=batman&page=1:

(mock/request :get "/movies" {:search "batman" :page 1})

Новый пользователь из формы, POST /users. В этом запросе тело (ключ :body) станет классом ByteArrayInputStream. Заголовок Content-Type примет значение "application/x-www-form-urlencoded".

(mock/request :post "/users" {:name "Ivan" :email "test@test.com"})

Случай для JSON-API. Адрес /users ожидает не поля формы, а JSON-тело.Такой запрос составляют из двух функций:

(-> (mock/request :post "/users")
    (mock/json-body {:name "Ivan" :email "test@test.com"}))

Проверка тела. В тестах выше мы проверяли только статус ответа. На практике одного статуса недостаточно: число 200 еще не говорит, что в ответе именно то, что нужно. Проверка тела зависит от типа содержимого. Если это текст или HTML, иногда хватит и регулярного выражения. Например, по фразе “Login” мы определим, что на этой странице пользователь не авторизован.

Гораздо интересней вариант с JSON-API. В этом случае нужно восстановить данные из ответа и сравнить с образцом. Для простоты вызовем приложение sites-handler. Это заглушка, которой мы пользовались для тестировании карт. Проверка ниже гарантирует, что при изменении ответа мы получим ошибку. По-другому говорят, что ответ зафиксирован:

(let [request (mock/request :get "/search/v1/" {:lat 11 :lon 22})
      response (sites-handler request)
      body (-> response :body (cheshire.core/parse-string true))]
  (is (= {...} body)))

Недостаток в том, что мы сравниваем данные как есть. Прием не сработает, если в ответе даты, уникальные идентификаторы (UUID) или машинные номера. Перед сравнением их исключают с помощью dissoc и map. Представим, что поиск кафе вернул результат в таком виде:

{:sites [{:name "Site1" :date-updated "2019-11-12" :id 42}
         {:name "Site2" :date-updated "2019-11-10" :id 99}]}

Код ниже вернет только те данные, которые не меняются от запроса к запросу. Их и сравнивают с образцом.

(update body :sites
        (fn [sites]
          (map (fn [site]
                 (dissoc site :id :date-updated))
               sites)))
;; {:sites ({:name "Site1"} {:name "Site2"})}

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

(let [;; obtain the response
      body (-> response :body (cheshire.core/parse-string true))]
  (is (s/valid? :api.search/handler body)))

Затраты на спеку или схему окупаются в будущем. Ими проверяют входные параметры, генерируют данные для тестов, описывают REST API (Swagger, RAML).

Тестирование систем

Коротко о том, как пишут тесты в проектах с системами, о которых мы говорили в отдельной главе. Напомним, система это набор компонентов со связями между ними. Покрыть каждый компонент тестами нетрудно; проблемы возникают на стыке при взаимодействии. В проекте обязательно должен быть тест, где система работает как единое целое.

Очевидно, чтобы тест прошел, кто-то должен запустить систему и остановить ее после. На эту роль подходит фикстура. Будем полагать, что переменная системы и функции ее запуска и останова находятся в модуле system.clj. Напишем фикстуру fix-system:

(defn fix-system [t]
  (system/start!)
  (t)
  (system/stop!))

На время ее работы в переменной system/system будет рабочая система. Другие фикстуры, например, для работы с базой, могут обратиться к ее компонентам напрямую. Важно только, чтобы в вызове use-fixtures они шли в правильном порядке (левее — раньше), иначе вы получите Null-ошибки и другие неприятности.

(defn fix-db-data [t]
  (let [{:keys [db]} system/system]
    (prepare-test-data db)
    (t)
    (clear-test-data db)))

(use-fixtures :once fix-system fix-db-data)

Читатель заметит, что это противоречит правилу, о котором мы говорили в главе про системы. Что к системе нельзя обращаться напрямую и копаться в ее компонентах. Что ж, признаем, для тестов в этом плане действуют послабления. Обращаться к системе нельзя в промышленном коде. Но тесты это не промышленный код, поэтому на небольшие нарушения в них порой закрывают глаза. Главное, чтобы программист понимал, что он намерен выиграть. В нашем случае выигрыш в том, что мы не пробрасываем компонент базы в фикстуру. Это было бы честнее, но заняло больше строк кода.

Фикстура fix-system неслучайно стоит под ключом :once. Запуск и остановка системы занимают много времени. В наших интересах прогнать как можно больше тестов, пока система работает. Если же делать это поштучно, процесс затянется надолго. Именно с этой проблемой вы столкнетесь при запуске тестов из CIDER. При попытке выполнить один тест вы будете ждать, пока сработают все фикстуры, в том числе fix-system.

Кажется, что пять-десять секунд это немного. Но представьте, что работаете над задачей и запускаете тест раз за разом — подобные паузы раздражают и сбивают с ритма. Поделимся с читателем техникой, которая решит эту проблему.

Потребуется два шага. Первый — научить систему, чтобы она знала о своем состоянии. Например, включена ли она сейчас или выключена. Проще всего это сделать через поле с метаданными. Вынесем имя поля в отдельную переменную. Перепишем start! так, чтобы в метаданных системы появился флаг со значением true. Аналогично работает stop!, только флаг принимает ложь. Функция started? возвращает флаг значение из текущей системы.

(def state-field ::started?)

(defn start! []
  (let [sys (-> system
                component/start-system
                (with-meta {state-field true}))]
    (alter-var-root #'system (constantly sys))))

(defn started? []
  (some-> system meta (get state-field)))

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

(defn fixture-system [t]
  (let [started-manually? (system/started?)]
    (when-not started-manually?
      (start!))
    (t)
    (when-not started-manually?
      (stop!))))

Выполните в REPL (system/start!). Теперь можно вызвать тест сколько угодно раз — не придется ждать, пока запустится система.

Интеграционные тесты

Не протяжении главы мы постепенно усложняли тесты. С каждым шагом они все меньше зависят от технических деталей и делают упор на бизнес-логику. По-другому этот принцип называют пирамидой тестов. В ее основании лежат юнит-тесты — множество отдельных проверок. Поднимаясь к вершине, мы абстрагируемся от технического слоя: в какой-то момент тестируем не отдельные функции, а часть приложения.

Каждый уровень в пирамиде требует специальных знаний. Полагаем, читатель готов к тому, чтобы подняться на последний этаж — освоить интеграционное тестирование. Это особая фаза тестов, когда окружение максимально похоже на промышленное. По-другому их еще называют UI- или Selenium-тестами в честь одноименного фреймворка. Для большей реалистичности мы не шлем запросы программно, а имитируем действия человека. Например, управляем браузером: вводим данные в форму, нажимаем кнопку и проверяем, что появился нужный текст.

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

В этом разделе мы рассмотрим, как писать UI-тесты на Clojure. С подготовительной частью вы уже знакомы: запускаем систему и наполняем базу тестовыми данными. А вот тест ведет себя по-новому. Он захватывает контроль над браузером и командует им. Например, открывает страницу http://127.0.0.1:8080/ и щелкает по ссылкам. В любой момент мы получим адрес страницы, ее заголовок и HTML-код. В тест добавляют формы (is (= ...)), чтобы проверить, на какой странице мы оказались или видит ли пользователь этот виджет.

Чтобы управлять браузером, понадобится драйвер и библиотека к нему. Под драйвером понимают утилиту командной строки. Когда драйвер запущен, он принимает запросы от библиотеки по протоколу HTTP. Одновременно драйвер запускает браузер в особом режиме, и между ними образуется связь. Фактически, драйвер это посредник между двумя акторами. Его работа сводится к переводу HTTP-команд в бинарный протокол браузера и наоборот.

Каждый браузер работает со своим драйвером. Для Chrome он называется chromedriver, для FireFox — geckodriver. Одноименные утилиты ставятся из пакетных менеджеров apt, yum или brew в зависимости от операционной системы. Пользователи Windows скачают бинарные файлы с сайта проекта. Драйвер к Safari называется safaridriver. С версии 13 он идет в комплекте с Mac OS.

Для работы с драйвером подойдет библиотека Etaoin. Добавьте ее в зависимости профиля :dev (библиотека нужна только для тестов):

:dev {:dependencies [[etaoin "0.3.6"]]}

Убедитесь, что драйвер находится в одной из папок, перечисленных в PATH, например, /usr/local/bin. Другими словами, его можно вызвать в терминале, просто выполнив chromedriver или geckodriver. Путь до драйвера можно задать в опциях библиотеки, но пока что мы сами положим его в нужное место.

Теперь напишем первый тест. Представим, что локальный сервер работает на порту 8080. Тест открывает форму входа, заполняет поля и нажимает кнопку “Login”. Страница обновляется, наверху видно приветствие. Пользователь видит элементы, которые прежде были скрыты (ссылки “My profile”, “Logout”).

(ns project.integration-tests
  (:require [etaoin.api :as e]))

(deftest test-ui-login-ok
  (e/with-chrome {} driver
    (e/go driver "http://127.0.0.1:8080/login")
    (e/wait-visible driver {:fn/has-text "Login"})
    (e/fill driver {:tag :input :name :email} "test@test.com")
    (e/fill driver {:tag :input :name :password} "J3QQ4-H7H2V")
    (e/click driver {:tag :button :fn/text "Login"})
    (e/wait-visible driver {:fn/has-text "Welcome"})
    (is (e/visible? driver {:tag :a :fn/text "My profile"}))
    (is (e/visible? driver {:tag :button :fn/text "Logout"}))))

Разберем отдельные выражения из этого теста. Форма with-chrome это макрос, который запускает Хром на время исполнения тела. Макрос необходим, чтобы гарантированно закрыть драйвер при выходе или в случае ошибки. Без него пришлось бы писать код с ручным try/catch, что порождает вложенность и в целом неудобно:

(let [driver (e/chrome)]
  (try
    (e/go driver "http://...")
    (e/click driver {:tag :button})
    (finally
      (e/quit driver))))

Функция wait-visible ждет до тех пор, пока элемент не появится на экране. К ней прибегают довольно часто, чтобы дождаться, пока браузер отрисует верстку. Если не разделить две инструкции ожиданием, между ними будет разница в несколько миллисекунд. Второе действие либо не успеет выполниться, либо будет отброшено.

Ожидание в UI-тестах это нормально. Основное время уходит на то, чтобы дождаться загрузки и выполнить действия с задержкой, как это свойственно человеку. Wait-visible это лишь одна из семейства wait-функций. В их число входит wait-has-text (дождаться текст на экране), wait-has-class (ждать, пока у элемента не появится класс) и многие другие.

Драйвер ищет элементы на странице с помощью селекторов. Это строки с особыми выражениями; различают CSS- и XPath-селекторы. Сейчас мы не будем разбирать их синтаксис: это долго и заслуживает отдельной главы. Для простоты рассмотрим альтернативный прием. На элемент можно сослаться, задав его свойства словарем. Ключи tag и id означают имя тега и его идентификатор. Другие ключи становятся атрибутами тега. В примере выше селектор {:tag :input :name :email} становится выражением input[@name='email'] на языке XPath.

Подумаем, как улучшить наш тест? Для начала рассмотрим порт 8080. Мы уже знаем, что подобные значения приходят из конфигурации. Исправьте тест так, чтобы и сервер, и драйвер работали с одним и тем же портом.

Вспомним, как работает with-chrome: он создает новый драйвер, выполняет тело и выключает его. Если каждый тест обернут в with-chrome, мы теряем время, многократно повторяя эти шаги. Сделаем так, чтобы драйвер работал на протяжении всего прогона тестов. Объявим динамическую переменную и фикстуру, которая связывает драйвер на время тестов. Зарегистрируем ее с ключом :once.

(defonce ^:dynamic *driver* nil)

(defn fix-chrome [t]
  (e/with-chrome {} driver
    (binding [*driver* driver]
      (t))))

Мы тестируем приложение в Хроме, самом популярном браузере. Но вот приходит задача — убедиться, что мы также поддерживаем Firefox. Технически это значит, что все тесты, которые мы написали для Хрома, должны сработать еще раз в другом браузере. Скопировать их и заменить with-chrome на with-firefox — сомнительное решение. Представьте, что через месяц нас попросят добавить Safari. Должен быть способ, который не влечет за собой разрастание кода.

Поможет мульти-фикстура, с которой мы знакомились в середине главы. Она пробегает по списку типов браузеров; в примере ниже это :firefox и :chrome. Макрос with-driver это общий случай with-chrome и аналогов. Отличие в том, что with-driver ожидает первым аргументом тип браузера. На каждом шаге фикстура связывает драйвер и выполняет тест.

(defn fix-multi-driver [t]
  (doseq [driver-type [:firefox :chrome]]
    (e/with-driver driver-type {} driver
      (binding [*driver* driver]
        (testing (format "Browser %s" (name driver-type))
          (t))))))

Теперь тесты по очереди сработают в каждом из браузеров. Для ясности мы оборачиваем тест в сообщение о том, в рамках какого браузера его вызывают. Поддержка нового браузера сводится к тому, чтобы добавить в список ключ :safari, :edge и другие.

Еще один способ улучшить тесты — вынести одинаковые действия в фикстуру или функцию. Например, каждый тест начинается с авторизации и заканчивается выходом из системы. Чтобы не копировать эти действия каждый раз, напишем фикстуру fix-login-logout. В отличии от предыдущих фикстур, ее регистрируют с ключом :each.

(defn fix-login-logout [t]
  (doto *driver*
    (e/go "http://127.0.0.1:8080/login")
    (e/fill {:tag :input :name :email} "test@test.com")
    (e/click {:tag :button :fn/text "Login"}))
  (t)
  (doto *driver*
    (e/click {:tag :button :fn/text "Logout"})
    (e/wait-has-text "Login")))

Попутно мы внедрили еще одну хорошую практику. Когда несколько функций принимают одинаковый первый аргумент, их объединяют в макрос doto. Он подставит *driver* на второе место в каждый список тела. С doto код становится немного короче и чище.

Другие решения

В последнем разделе мы перечислим другие библиотеки, полезные для тестов. Мы не будем рассматривать их досконально: ограничимся кратким описанием и примером кода. Все ответы ищите в документации к проектам.

Продвинутые моки

На минуту вернемся к мокам — подмене функции через with-redefs. Этот макрос слишком многословен, чтобы работать с ним напрямую. Появились библиотеки, которые описывают мокинг короче и выразительнее. Одна из них называется mockery. Библиотека предлагает макрос with-mock следующего вида:

(with-mock mock
  {:target :project.path/get-geo-point
   :return {:lat 14.23 :lng 52.52}}
  (get-geo-point "cafe" "200m"))

В примере выше мы “замокали” get-geo-point, которая, судя по названию, обращается к стороннему сервису карт. Внутри макроса объект mock это атом, внутри которого словарь. Он наполняется данными по мере того, как мы вызываем цель. Например, сколько раз ее вызвали и с какими аргументами. В выражении ниже мы добавили проверки на то, что функцию вызвали один раз с аргументами “cafe” и “200m”.

(let [{:keys [called? call-count call-args]} @mock]
  (is called?)
  (is (= 1 call-count))
  (is (= '("cafe" "200m") call-args)))

Библиотека spy устроена похожим образом. На функцию навешивается “шпион”, который копит данные о вызове.

(defn adder [x y] (+ x y))
(def spy-adder (spy/spy adder))

(testing "calling the function"
  (is (= 3 (spy-adder 1 2))))

(testing "calls to the spy can be accessed via spy/calls"
  (is (= [[1 2]] (spy/calls spy-adder))))

Альтернативный синтаксис

Проект midje предлагает другой способ писать тесты. В этой библиотеке мы имеем дело с фактами. Факт это набор проверок, сгруппированных по смыслу. В примере ниже факты о функции split:

(facts "about `split`"
 (str/split "a/b/c" #"/") => ["a" "b" "c"]
 (str/split "" #"irrelvant") => [""])

Стрелка между выражениями это особый оператор, который называется extended equality, продвинутое равенство. С ее помощью сравнение величин записывается короче. Например, форма 1 => even? приходит к виду (even? 1). В midje встречаются и другие, более сложные стрелки для проверки коллекций и макросов.

Вывод XUnit

Плагин test2junit делает так, что отчет о тестах пишется в XML-файл формата XUnit. Системы непрерывной интеграции, например, CircleCI или TeamCity понимают, как отобразить его графически. Такой отчет легче просматривать, чем вывод консоли. Проблемные места выделены красным, стектрейсы спрятаны под выпадающие элементы. Плагину нужно задать путь к папке, куда писать файл.

:plugins [[test2junit "1.1.2"]]
:test2junit-output-dir "target/test2junit"

Генерация данных

Возможно, вы столкнетесь с тем, что для тестов нужен большой объем данных, например, сто или двести тысяч записей. При этом данные должны быть разнообразны — нас не устроит один и тот же набор, скопированный тысячу раз. Поможет библиотека test.check. Ее модуль gen генерирует случайные данные по заданным правилам. Особенно полезна генерация записей. В примере ниже мы получаем кортеж строки, числа и булева типа. Затем применяем его к конструктору ->User.

(defrecord User [user-name user-id active?])

(def user-gen
  (gen/fmap (partial apply ->User)
            (gen/tuple (gen/not-empty gen/string-alphanumeric)
                       gen/nat
                       gen/boolean)))

(last (gen/sample user-gen))
;; => #user.User{:user-name "dfgJKSHF3"
;;               :user-id 5
;;               :active? false}

Библиотека clojure.spec, которой мы посвятили главу, идет еще дальше. С помощью test.check она генерирует данные по спеке. Так проявляется еще одно свойство спек: кроме проверки, они подходят для тестовых данных.

(s/def :user/id int?)
(s/def :user/name string?)
(s/def :user/active? boolean?)
(s/def ::user (s/keys :req-un [:user/id :user/name :user/active?]))

(gen/generate (s/gen ::user))
{:id 88546920, :name "Z4MO7GH80k3mRD", :active? true}

Возможности spec.gen обширны. С ее помощью порождают связанные данные, например, пользователей, которые ссылаются на профили и наоборот. Вместо случайных величин можно опираться на допустимые значения (списки имен, городов). Спеки бывают быть любой вложенности, что открывает поле для экспериментов.

Порядок аргументов

Необычный вопрос: как писать правильно, (is (= 200 status)) или (is (= status 200))? На первый взгляд это абсурд. Разве может порядок аргументов влиять на равенство? Значения либо равны, либо нет. Однако, макрос is устроен сложнее, чем мы думаем. Он разбирает выражение (= 200 status) и выделяет ожидаемую и фактическую части. По-английски они называются expected и actual.

Ожидаемое это значение, на которое рассчитывает тест. Как правило, это готовое число или коллекция, которую посчитали заранее. Фактическое значение — то, к которому мы пришли самостоятельно, например, вызвав функцию. Так, число 68 это ожидаемое, а (int (->fahr 20)) — действительное. Статус 200 это ожидаемое, а (:status response) — действительное.

Такое разделение необходимо для отчетов. Когда значения не равны, нам бы хотелось увидеть, где мы ошиблись. Предположим, что в отчете написано: failed (= 200 403). Не совсем ясно, как это трактовать. Мы ожидали успешный ответ, но не хватило прав доступа? Или это брешь в безопасности — ожидали, что прав на эту страницу нет, но пользовать все-таки ее увидел? Если же написано expected 200, got 403, то все ясно — это первый случай (не хватило прав).

Теперь запомните правило: ожидаемое стоит на первом месте, а действительное на втором. Поэтому пишите (is (= 200 status)) вместо (is (= status 200)). Автор согласен, что это непривычно и противоречит здравому смыслу. Как правило, фактическое это число, а действительное — длинное выражение, поэтому хочется записать их как слева. Увы, придется побороть себя и писать как справа:

;; wrong                     ;; correct
(= (int (->fahr 20)) 68)     (= 68 (int (->fahr 20)))

Это не ошибка дизайна; правило уходит корнями в прошлое. Первый тестовый фреймворк JUnit утвердил именно такой порядок в методах сравнения. Хорошо это или плохо, судить уже поздно — принцип “expected слева” стал промышленным стандартом. Аналогичное правило работает в языках Python, Ruby и других. Отдельные фреймворки предлагают модули, чтобы “перестать говорить как Йодо”, то есть поменять семантику аргументов. Технически это возможно и в Clojure, но сейчас мы не будем это рассматривать.

Особенность expected и actual видна при запуске тестов в CIDER. Один и тот же тест проверяет статус ответа на 200. Пока все идет хорошо, нет разницы, в каком порядке мы записали аргументы. Но в случе ошибки вариант слева вносит путаницу. Согласно ему, нормальным считается статус 404, а не 200. Вариант справа выводит статусы правильно.

;; wrong                ;; correct
(is (= status 200))     (is (= 200 status))

Fail in test-...        Fail in test-...
expected: 404           expected: 200
  actual: 200             actual: 404
    diff: - 404             diff: - 200
          + 200                   + 404

Тем не менее, не стоит соблюдать это правило слишком рьяно. Иногда равенство с перепутанными аргументами смотрится лучше, и потому код легче поддерживать. Следить за порядком или нет остается на усмотрение команды. Автор признаётся, что на этапе черновика перепутал аргументы везде, где это возможно.

Permalink

Senior UI Developer

Senior UI Developer

Apstra | Menlo Park, CA

Apstra is looking for UI engineers to work on our growing platform. Our core product is an infrastructure management tool that allows networking architects and operators to design, deploy, validate and operate large network infrastructures end-to-end.

Engineering challenges include building UI forms, visualizations, and interactions that will simplify complex Network operation workflows for the end-users.

Responsibilities

  • Working with product management and back-end engineering teams to define how new features will work from the UI perspective based on user stories and available API definitions
  • Implement new features in ClojureScript and re-frame
  • Working with testing teams to optimize UI for testing automation
  • Optimize UI architecture and UX to work well for large scale deployments and growing amounts of real-time data

Requirements

Must

  • Recent experience working with ClojureScript or experience building front-ends written in a functional style and willingness to learn ClojureScript
  • Experience building medium to large size Single-page applications (SPAs)
  • Experience working with RESTful and REST-like APIs
  • Understanding of JavaScript and React ecosystems
  • Ability to take ownership of the feature and drive its delivery with minimal supervision
  • Ability to create UIs based on a minimal set of requirements

Nice to have

  • Experience using Reagent and re-frame
  • Experience building visualizations for real-time data
  • Understanding of Networking and/or Infrastructure Management domains
  • Basic design skills
  • Experience working in a startup environment

What We Do

Apstra pioneered Intent-Based Networking for the data center and Intent-Based Analytics.

Apstra's intent-based networking for the data center enables enterprises, cloud providers, and telcos to increase application reliability and availability simplifies deployments and operations through automation, and dramatically reduces OpEx and CapEx costs. Apstra enables vendor-agnostic data center lifecycle automation and is deployed by many businesses around the world including Fortune 100 Enterprises. These companies are taking advantage of game-changing agility, reliability and significantly lower TCO with complete flexibility to deploy the hardware and network operating system of their choice. Apstra is headquartered in Menlo Park, California and privately funded. Apstra is a Gartner Cool Vendor and Best of VMworld winner.

For more information visit: https://www.apstra.com/company/careers/

Permalink

Algebra is about composition

When we look at the definitions of algebraic properties, we often see that we are defining how things compose. This is one of the main advantages of using algebraic properties to constrain our operations. If we define how they should compose before we implement them (as a unit test, for instance) we can guarantee that things will compose.

Video Thumbnail

Algebra is about composition

When we look at the definitions of algebraic properties, we often see that we are defining how things compose. This is one of the main advantages of using algebraic properties to constrain our operations. If we define how they should compose before we implement them (as a unit test, for instance) we

The post Algebra is about composition appeared first on LispCast.

Permalink

PurelyFunctional.tv Newsletter 352: Tip: use the right kind of comment for the job

Issue 352 – November 18, 2019 · Archives · Subscribe

First Annual PurelyFunctional.tv Survey! 📋

I’m happy to announce the first annual PurelyFunctional.tv Survey. Your answer to this quick survey will help me understand how to improve my videos and help you master Clojure faster and more deeply.

There are only four questions. If you’ve watched any of my video content, please take a few minutes to fill this out. I appreciate any answer you can give.

Fill out the survey

The survey will run for a few weeks.

Clojure Tip 💡

use the right comment for the job

Let’s face it: sometimes commenting out code is useful. Sometimes you want to test a large piece of code without launching the missile just yet. Comment out the code! And many Clojurists keep snippets of code in a comment at the end of a file to facilitate Repl-Driven Development. And then, of course, there are comments that are actually comments on the code.

Clojure has three ways to ignore input. Each is different in significant ways. Each has found a use in the community. Let’s go over each of them.

Line comments

If you put a semicolon in your code (anywhere outside of a string or a character literal) it starts a comment that goes till the end of the line. Anything characters are allowed in that comment. It won’t be read or parsed, just ignored. People use these for two purposes:

1. To add a note in natural language to the code.

;; 1002 works. I don't know why. Change at your own risk.
(run-me 1002) ; here is the magic number

As a community, and in many editors, double semicolons indicate comments that shouldn’t be grossly indented. Single semicolons are for comments that come after other code on the same line.

2. To comment out a single line of code.

Sometimes the code you want to comment out is all on one line at the end of the line, and you just want to insert a single ;. Go for it.

(doseq [task tasks]
  ;;(println task)
  (run-task task))

comment macro

There’s a macro called comment that will comment out any number of forms. All you have to do is surround the forms with the comment macro. There are some gotchas:

  1. It’s a macro, so the forms inside have to be valid for the Clojure reader. That means you have to obey all the syntax rules for numbers, symbols, and braces matching.
  2. The comment form itself is still an expression in your program. It evaluates to nil. That could have an effect on any surrounding forms.

In short, it’s just a way to avoid executing code, but not useful for text. It’s also not very useful for commenting out sub-expressions since it evaluates to nil.

So what do people use this for? Mostly for code they want to run during development but not in the normal flow of production code. It’s common to put a comment block at the end of a file with expressions to evaluate for testing.

It sounds weird, but it’s better than typing in the same expressions over and over into the REPL prompt. Most editors have a setting so you can evaluate expressions inside the comment just like top-level expressions.

#_ reader macro

Clojure’s reader has a reader macro (different from a regular macro) that directs the reader to ignore the next form. That is, it reads a form and then throws it away. It’s a nice way to ignore an entire expression.

#_(println a
    b
    c
    d)

Notice that this one works really well for multi-line expressions. Unlike ;, which ignores everything up to the end of the line, #_ ignores the entire next expression. It’s the perfect way to “turn off” one expression. It even works for sub-expressions—it is truly ignored by the reader, not replaced with nil. It’s great for dropping an argument, for instance.

But get this: they stack. Often, we want to ignore two expressions, not just one. Here’s a case:

(def mappings {:a 1 :b 2 :c 3})

What if we want to get rid of the mapping of :a to 1? We could do this:

(def mappings {#_:a #_1 :b 2 :c 3})

But because #_ ignores the next expression, there is a better way. Two #_s will ignore the next 2 expressions:

(def mappings {#_#_:a 1 :b 2 :c 3})

Now there’s just one thing to delete if you want to turn it back on.

Thanks to Ray McDermott and the rest of the panel at Apropos Clojure for this final tip.

Conference alert 🚨

I booked my tickets to the 2019 Clojure/conj in Durham, North Carolina. If you’re going, let me know! I haven’t been to a Conj in a couple of years and I’m really excited.

Ask me about my book and I’ll give you a discount code.

I am considering bringing a board game to play at the board game night.

Currently recording 🎥

Property-Based Testing with test.check is now split into three courses, Beginning, Intermediate, and Advanced.

Last week I promised a flood but all I’ve got is a trickle. Coming back from vacation is hard :). However, they are now available to purchase for a low price for the launch sale. I will end the sale after the Conj. That gives you two weeks to buy them. They will never be this inexpensive again!

Book update 📖

I have been getting such great stories from readers of Grokking Simplicity. People tell me they are making their code more organized and testable. After all of the work I put into the book, it feels good that it’s having such a great impact.

You can buy the book and use the coupon code TSSIMPLICITY for 50% off.

Thanks for reading.

Clojure Challenge 🤔

Last week’s challenge

Well, I got no submissions from last week.

This week’s challenge

Write a program that outputs all possibilities to put a + or – or nothing between the numbers 1-9 (in order) such that the result is 100. For example:

1 + 2 + 34 - 5 + 67 - 8 + 9 = 100

The function should output a list of strings with these formulas.

Hint: this is a recursive problem. Use divide and conquer.

As usual, please reply to this email to send me your submissions. I’ll collect them up and share them in the next issue. If you don’t want me to share your submission, let me know.

Rock on!
Eric Normand

The post PurelyFunctional.tv Newsletter 352: Tip: use the right kind of comment for the job appeared first on PurelyFunctional.tv.

Permalink

Reporting on Kafka Connect Jobs

At the risk of diluting the brand message (i.e. testing kafka stuff using Clojure), in this post, I’m going to introduce some code for extracting a report on the status of Kafka Connect jobs. I’d argue it’s still “on-message”, falling as it does under the observability/metrics umbrella and since observability is an integral part of testing in production then I think we’re on safe ground.

I know I promised a deep-dive on the test-machine journal but it’s been a crazy week and I needed to self-sooth by writing about something simpler that was mostly ready to go.

Kafka Connect API

The distributed version of Kafka Connect provides an HTTP API for managing jobs and providing access to their configuration and current status, including any errors that have caused the job to stop working. It also provides metrics over JMX but that requires

  1. Server configuration that is not enabled by default
  2. Access to a port which is often only exposed inside the production stack and is intended to support being queried by a “proper” monitoring system

This is not to say that you shouldn’t go ahead and setup proper monitoring. You definitely should. But you needn’t let the absence of it prevent you from quickly getting an idea of overall health of your Kafka Connect system.

For this script we’ll be hitting two of the endpoints provided by Kafka Connect

GET /connectors

Here’s the function that hits the /connectors endpoint. It uses Zach Tellman’s aleph and manifold libraries. The http/get function returns a deferred that allows the API call to be handled asynchronously by setting up a “chain” of operations to deal with the response when it arrives.

(ns grumpybank.observability.kc
 (:require
   [aleph.http :as http]
   [manifold.deferred :as d]
   [clojure.data.json :as json]
   [byte-streams :as bs]))

(defn connectors
  [connect-url]
  (d/chain (http/get (format "%s/connectors" connect-url))
    #(update % :body bs/to-string)
    #(update % :body json/read-str)
    #(:body %)))

GET /connectors/:connector-id/status

Here’s the function that hits the /connectors/:connector-id/status endpoint. Again, we invoke the API endpoint and setup a chain to deal with the response by first converting the raw bytes to a string, and then reading the JSON string into a Clojure map. Just the same as before.

(defn connector-status
  [connect-url connector]
  (d/chain (http/get (format "%s/connectors/%s/status"
                             connect-url
                             connector))
    #(update % :body bs/to-string)
    #(update % :body json/read-str)
    #(:body %)))

Generating a report

Depending on how big your Kafka Connect installation becomes and how you deploy connectors you might easily end up with 100s of connectors returned by the request above. Submitting a request to the status endpoint for each one in serial would take quite a while. On the other-hand, the server on the other side is capable of handling many requests in parallel. This is especially true if there are a few Kafka Connect nodes co-operating behind a load-balancer.

This is why it is advantageous to use aleph here for the HTTP requests instead of the more commonly used clj-http. Once we have our list of connectors, we can fire off simultaneous requests for the status of each connector, and collect the results asynchronously.

(defn connector-report
  [connect-url]
  (let [task-failed? #(= "FAILED" (get % "state"))
        task-running? #(= "RUNNING" (get % "state"))
        task-paused? #(= "PAUSED" (get % "state"))]
    (d/chain (connectors connect-url)
      #(apply d/zip (map (partial connector-status connect-url) %))
      #(map (fn [s]
              {:connector (get s "name")
               :failed? (failed? s)
               :total-tasks (count (get s "tasks"))
               :failed-tasks (->> (get s "tasks")
                                  (filter task-failed?)
                                  count)
               :running-tasks (->> (get s "tasks")
                                   (filter task-running?)
                                   count)
               :paused-tasks (->> (get s "tasks")
                                  (filter task-paused?)
                                  count)
               :trace (when (failed? s)
                        (traces s))}) %))))

Here we first define a few helper predicates (task-failed?, task-running?, and task-paused?) for classifying the status eventually returned by connector-status. Then we kick off the asynchronous pipeline by requesting a list of connectors using connectors.

The first operation on the chain is to apply the result to d/zip which as described above will invoke the status API calls concurrently and return a vector with all the responses once they are all complete.

Then we simply map the results over an anonymous function which builds a map out of with the connector id together with whether it has failed, how many of its tasks are in each state, and when the connector has failed, the stacktrace provided by the status endpoint.

If you have a huge number of connect jobs you might need to split the initial list into smaller batches and submit each batch in parallel. This can easily be done using Clojure’s built-in partition function but I didn’t find this to be necessary on our fairly large collection of kafka connect jobs.

Wrap these functions up in a simple command line script and run it after making any changes to your kafka-connect configuration to make sure everything is still hunky-dory.

Here’s a gist that wraps these functions up into a quick and dirty script that reports the results to STDOUT. Feel free to re-use, refactor, and integrate with your own script to make sure after making changes to your deployed Kafka Connect configuration, everything remains hunky-dory.

Permalink

Ep 055: Sets! What Are They Good For?

Each week, we discuss a different topic about Clojure and functional programming.

If you have a question or topic you’d like us to discuss, tweet @clojuredesign, send an email to feedback@clojuredesign.club, or join the #clojuredesign-podcast channel on the Clojurians Slack.

This week, the topic is: “Sets! What are they good for?” We examine one of the lesser used data structures in Clojure and talk about its unique characteristics and uses.

Selected quotes:

  • “Sets aren’t going to get top billing.”
  • “Sets are really about uniqueness.”
  • “Sets let us calculate differences cheaply.”
  • “Clever is a word I used to like when I was a younger programmer.”
  • “Clever now equals hours of suffering!”

Permalink

Easy functional programming techniques in Rust for everyone

There is a lot of hype around functional programming(FP) and a lot of cool kids are doing it but it is not a silver bullet. Like other programming paradigms/styles, functional programming also has its pros and cons and one may prefer one paradigm over the other. If you are a Rust developer and wants to venture into functional programming, do not worry, you don't have to learn functional programming oriented languages like Haskell or Clojure(or even Scala or JavaScript though they are not pure functional programming languages) since Rust has you covered and this post is for you.

If you are looking for functional programming in Java, Golang or TypeScript check other posts in the series.

I'm not gonna dive into all functional programming concepts in detail, instead, I'm gonna focus on things that you can do in Rust which are in line with functional programming concepts. I'm also not gonna discuss the pros and cons of functional programming in general.

Please note that some introductions in this post are repeated from my other posts in the series for your ease of reading.

What is functional programming?

As per Wikipedia,

Functional programming is a programming paradigm—a style of building the structure and elements of computer programs—that treats computation as the evaluation of mathematical functions and avoids changing-state and mutable data.

Hence in functional programming, there are two very important rules

  • No Data mutations: It means a data object should not be changed after it is created.
  • No implicit state: Hidden/Implicit state should be avoided. In functional programming state is not eliminated, instead, its made visible and explicit

This means:

  • No side effects: A function or operation should not change any state outside of its functional scope. I.e, A function should only return a value to the invoker and should not affect any external state. This means programs are easier to understand.
  • Pure functions only: Functional code is idempotent. A function should return values only based on the arguments passed and should not affect(side-effect) or depend on the global state. Such functions always produce the same result for the same arguments.

Apart from these there are functional programming concepts below that can be applied in Rust, we will touch upon these further down.

Using functional programming doesn't mean its all or nothing, you can always use functional programming concepts to complement Object-oriented or imperative concepts in Rust. The benefits of functional programming can be utilized whenever possible regardless of the paradigm or language you use. And that is exactly what we are going to see.

Functional programming in Rust

Rust is primarily geared towards procedural/imperative style of programming but it also lets you do a little bit of functional and object-oriented style of programming as well. And that is my favorite kind of mix. So let us see how we can apply some of the functional programming concepts above in Rust using the language features.

First-class and higher-order functions

First-class functions(function as a first-class citizen) means you can assign functions to variables, pass a function as an argument to another function or return a function from another. Functions in Rust are a bit more complex than other languages, it's not as straightforward as in Go or JavaScript. There are different kinds of functions and two different ways of writing them. The first one is a function that cannot memoize its outer context and the second one is closures which can memoize its outer context. Hence concepts like currying and higher-order-functions are possible in Rust but may not be as easy to wrap your head around as in other languages. Also, functions that accept a closure can also accept a pointer to a function depending on the context. In many places, Rust functions and closures can be interchangeable. It would have been nicer if functions were simple and we could do all the below without having to rely on closures. But Rust chose these compromises for better memory safety and performance.

A function can be considered as a higher-order-function only if it takes one or more functions as parameters or if it returns another function as a result.
In Rust, this is quite easy to do with closures, it might look a bit verbose but if you are familiar with Rust then you should be fine.

fn main() {
    let list = vec![
        String::from("Orange"),
        String::from("Apple"),
        String::from("Banana"),
        String::from("Grape"),
    ];
    // we are passing the array and a closure as arguments to map_for_each method.
    let out = map_for_each(list, |it: &String| -> usize {
        return it.len();
    });

    println!("{:?}", out); // [6, 5, 6, 5]
}

// The higher-order-function takes an array and a closure as arguments
fn map_for_each(list: Vec<String>, fun: fn(&String) -> usize) -> Vec<usize> {
    let mut new_array: Vec<usize> = Vec::new();
    for it in list.iter() {
        // We are executing the closure passed
        new_array.push(fun(it));
    }
    return new_array;
}

There are also more complex versions that you can write with generics like below for an example

fn main() {
    let list = vec![2, 5, 8, 10];
    // we are passing the array and a closure as arguments to map_for_each method.
    let out = map_for_each(list, |it: &usize| -> usize {
        return it * it;
    });

    println!("{:?}", out); // [4, 25, 64, 100]
}

// The higher-order-function takes an array and a closure as arguments, but uses generic types
fn map_for_each<A, B>(list: Vec<A>, fun: fn(&A) -> B) -> Vec<B> {
    let mut new_array: Vec<B> = Vec::new();
    for it in list.iter() {
        // We are executing the closure passed
        new_array.push(fun(it));
    }
    return new_array;
}

But then we could also simply do it this way using built-in functional methods like map, fold(reduce) and so on which is much less verbose. Rust provides a lot of useful functional style methods for working on collections like map, fold, for_each, filter and so on.

fn main() {
    let list = ["Orange", "Apple", "Banana", "Grape"];

    // we are passing a closure as arguments to the built-in map method.
    let out: Vec<usize> = list.iter().map(|x| x.len()).collect();

    println!("{:?}", out); // [6, 5, 6, 5]
}

Closures in Rust can memorize and mutate its outer context but due to the concept of ownership in Rust, you cannot have multiple closures mutating the same variables in the outer context. Currying is also possible in Rust but again due to ownership and lifetime concepts, it might feel a bit more verbose.

fn main() {
    // this is a higher-order-function that returns a closure
    fn add(x: usize) -> impl Fn(usize) -> usize {
        // A closure is returned here
        // variable x is obtained from the outer scope of this method and memorized in the closure by moving ownership
        return move |y| -> usize { x + y };
    };

    // we are currying the add method to create more variations
    let add10 = add(10);
    let add20 = add(20);
    let add30 = add(30);

    println!("{}", add10(5)); // 15
    println!("{}", add20(5)); // 25
    println!("{}", add30(5)); // 35
}

Pure functions

As we saw already a pure function should return values only based on the arguments passed and should not affect or depend on the global state. It is possible to do this in Rust easily.

Take the below, this is a pure function. It will always return the same output for the given input and its behavior is highly predictable. We can safely cache the method if needed.

fn sum(a: usize, b: usize) -> usize {
    return a + b;
}

But since Rust variables are immutable by default, unless specified a function cannot mutate any variables passed to it and cannot capture any variable in its context. So if we try to affect external state like below the compiler will complain "can't capture dynamic environment in a fn item"

use std::collections::HashMap;

fn main() {
    let mut holder = HashMap::new();

    fn sum(a: usize, b: usize) -> usize {
        let c = a + b;
        holder.insert(String::from(format!("${a}+${b}", a = a, b = b)), c);
        return c;
    }
}

In Rust, in order to capture external state, we would have to use closures, so we can rewrite the above as

use std::collections::HashMap;

fn main() {
    let mut holder = HashMap::new();

    let sum = |a: usize, b: usize| -> usize {
        let c = a + b;
        holder.insert(String::from(format!("${a}+${b}", a = a, b = b)), c);
        return c;
    };

    println!("{}", sum(10, 20));
}

But the compilation will still fail with the message "cannot borrow sum as mutable, as it is not declared as mutable". So in order to do external state mutation, we would have to explicitly specify the function as mutable like let mut sum = ...

So Rust will help you keep your functions pure and simple by default. Of course, that doesn't mean you can avoid side effects that don't involve variable mutations, for those you have to take care of it yourself.

Recursion

Functional programming favors recursion over looping. Let us see an example for calculating the factorial of a number.

In traditional iterative approach:

fn main() {
    fn factorial(mut num: usize) -> usize {
        let mut result = 1;
        while num > 0 {
            result *= num;
            num = num - 1;
        }
        return result;
    }

    println!("{}", factorial(20)); // 2432902008176640000
}

The same can be done using recursion as below which is favored in functional programming -- But recursion is not the solution always, for some cases a simple loop is more readable.

fn main() {
    fn factorial(num: usize) -> usize {
        return match num {
            0 => 1,
            _ => num * factorial(num - 1),
        };
    }

    println!("{}", factorial(20)); // 2432902008176640000
}

The downside of the recursive approach is that it will be slower compared to an iterative approach most of the times(The advantage we are aiming for is code simplicity and readability) and might result in stack overflow errors since every function call needs to be saved as a frame to the stack. To avoid this tail recursion is preferred, especially when the recursion is done too many times. In tail recursion, the recursive call is the last thing executed by the function and hence the functions stack frame need not be saved by the compiler. Most compilers can optimize the tail recursion code the same way iterative code is optimized hence avoiding the performance penalty. But unfortunately, Rust does not support this yet.

Consider using recursion when writing Rust code for readability and immutability, but if performance is critical or if the number of iterations will be huge use the standard iterative approach.

Lazy evaluation

Lazy evaluation or non-strict evaluation is the process of delaying the evaluation of an expression until it is needed. In general, Rust does strict/eager evaluation. We can utilize higher-order-functions, closures, and memoization techniques to do lazy evaluations.

Take this example where Rust eagerly evaluates everything.

fn main() {
    fn add(x: usize) -> usize {
        println!("executing add"); // this is printed since the functions are evaluated first
        return x + x;
    }

    fn multiply(x: usize) -> usize {
        println!("executing multiply"); // this is printed since the functions are evaluated first
        return x * x;
    }

    fn add_or_multiply(add: bool, on_add: usize, on_multiply: usize) -> usize {
        if add {
            on_add
        } else {
            on_multiply
        }
    }
    println!("{}", add_or_multiply(true, add(4), multiply(4))); // 8
    println!("{}", add_or_multiply(false, add(4), multiply(4))); // 16
}

This will produce the below output and we can see that both functions are executed always

executing add
executing multiply
8
executing add
executing multiply
16

We can use higher-order-functions to rewrite this into a lazily evaluated version

fn main() {
    fn add(x: usize) -> usize {
        println!("executing add"); // this is printed since the functions are evaluated first
        return x + x;
    }

    fn multiply(x: usize) -> usize {
        println!("executing multiply"); // this is printed since the functions are evaluated first
        return x * x;
    }

    type FnType = fn(t: usize) -> usize;

    // This is now a higher-order-function hence evaluation of the functions are delayed in if-else
    fn add_or_multiply(add: bool, on_add: FnType, on_multiply: FnType, t: usize) -> usize {
        if add {
            on_add(t)
        } else {
            on_multiply(t)
        }
    }
    println!("{}", add_or_multiply(true, add, multiply, 4)); // 8
    println!("{}", add_or_multiply(false, add, multiply, 4)); // 16
}

This outputs the below and we can see that only required functions were executed

executing add
8
executing multiply
16

You can also use memoization/caching techniques to avoid unwanted evaluations in pure and referentially transparent functions like below

use std::collections::HashMap;

fn main() {
    let mut cached_added = HashMap::new();

    let mut add = |x: usize| -> usize {
        return match cached_added.get(&x) {
            Some(&val) => val,
            _ => {
                println!("{}", "executing add");
                let out = x + x;
                cached_added.insert(x, out);
                out
            }
        };
    };

    let mut cached_multiplied = HashMap::new();

    let mut multiply = |x: usize| -> usize {
        return match cached_multiplied.get(&x) {
            Some(&val) => val,
            _ => {
                println!("executing multiply");
                let out = x * x;
                cached_multiplied.insert(x, out);
                out
            }
        };
    };

    fn add_or_multiply(add: bool, on_add: usize, on_multiply: usize) -> usize {
        if add {
            on_add
        } else {
            on_multiply
        }
    }

    println!("{}", add_or_multiply(true, add(4), multiply(4))); // 8
    println!("{}", add_or_multiply(false, add(4), multiply(4))); // 16
}

This outputs the below and we can see that functions were executed only once for the same values.

executing add
executing multiply
8
16

These may not look that elegant especially to seasoned Rust programmers. Fortunately, most of the functional APIs, like the iterators, provided by Rust do lazy evaluations and there are libraries like rust-lazy and Thunk which can be used to make functions lazy. Also, Rust provides some advanced types with which lazy evaluations can be implemented.

Doing Lazy evaluations in Rust might not be worth the code complexity some of the times, but if the functions in question are heavy in terms of processing then it is absolutely worth it to lazy evaluate them.

Type system

Rust is a strong statically typed language and also has great type inference. There are also advanced concepts like type aliasing and so on.

Referential transparency

From Wikipedia:

Functional programs do not have assignment statements, that is, the value of a variable in a functional program never changes once defined. This eliminates any chances of side effects because any variable can be replaced with its actual value at any point of execution. So, functional programs are referentially transparent.

Rust has great ways to ensure referential transparency, variables in Rust are immutable by default and even reference passing is immutable by default. So you would have to explicitly mark variables or references as mutable to do so. So in Rust, it is actually quite easy to avoid mutations.

For example, the below will produce an error

fn main() {
    let list = ["Apple", "Orange", "Banana", "Grape"];

    list = ["John", "Raju", "Sabi", "Vicky"];
}

And so does all of the below

fn main() {
    let list = vec![
        String::from("Orange"),
        String::from("Apple"),
        String::from("Banana"),
        String::from("Grape"),
    ];

    list.push(String::from("Strawberry")); // This will fail as the reference is immutable

    fn mutating_fn(val: String) {
        val.push('!'); // this will fail unless the argument is marked mutable reference or value passed is marked mutable reference
    }

    mutating_fn(String::from("Strawberry")); // this will fail if the reference is not passed as mutable
}

In order to compile these, we would have to riddle it with mut keywords

fn main() {
    let mut list = vec![
        String::from("Orange"),
        String::from("Apple"),
        String::from("Banana"),
        String::from("Grape"),
    ];

    list.push(String::from("Strawberry")); // This will work as the reference is mutable

    fn mutating_fn(val: &mut String) {
        val.push('!'); // this will work as the argument is marked as a mutable reference
    }

    mutating_fn(&mut String::from("Strawberry")); // this will work as the reference is passed as mutable
}

There are even more advanced concepts in Rust when it comes to data mutation and all that makes it easier to write immutable code.

Data structures

When using functional programming techniques it is encouraged to use data types such as Stacks, Maps, and Queues as they also have functional implementations. Hence Hashmaps are better than arrays or hash sets as data stores in functional programming. Rust provides such data types and is hence conforms to the functional specifications regarding data structures.

Conclusion

This is just an introduction for those who are trying to apply some functional programming techniques in Rust. There are a lot more that can be done in Rust. As I said earlier functional programming is not a silver bullet but it offers a lot of useful techniques for more understandable, maintainable and testable code. It can co-exist perfectly well with imperative and object-oriented programming styles. In fact, we all should be using the best of everything to solve the problem at hand instead of getting too obsessed about a single methodology.

I hope you find this useful. If you have any questions or if you think I missed something please add a comment.

If you like this article, please leave a like or a comment.

You can follow me on Twitter and LinkedIn.

Permalink

What do product and sum types have to do with data modeling?

Product and sum types allow us to exactly model any number of states with a lot of flexibility.

Video Thumbnail

What do product and sum types have to do with data modeling?

Product and sum types allow us to exactly model any number of states with a lot of flexibility. https://share.transistor.fm/e/fd5088aa https://www.youtube.com/watch?v=Mi30JdVAcH4 Transcript Eric Normand: What do product and sum types have to do with data modeling? By the end of this episode, I hope

Transcript

Eric Normand: What do product and sum types have to do with data modeling? By the end of this episode, I hope to answer that question, because I missed it when I talked about product and sum types the last time.

Hi, my name is Eric Normand and I help people thrive with functional programming. A few episodes back, I talked about product and sum types and explained what they were. I’m not going to go over that again, because you can go listen to that one and get it all.

I did get a couple questions about why I mentioned them, and what they have to do with data modeling at all. I realized I had explained what they were, but I totally forgot to talk about why you would want to use them at all. Now, I’m going to try to explain that.

Let’s look at a simple case, all right? You’re modeling this domain, and there’s certain cases that you have to capture. Let’s say that there are 10 cases.

Now, one thing you could do is simply enumerate all of the cases. Let’s say, it’s 10 states that the system can be in. You can enumerate them so you have one type that has all 10 cases. That would be a sum type that has the 10 different constructors, let’s say.

There’s other ways you can hit the number 10 as well. You could have two 5s. You could have two types, let’s say a five each, and then have an either between them. An either is a sum type, so it’s going to sum the five and the five. That gives you 10.

Now, another way you could do it is, you could have a two times five, so not five plus five, but two times five. Meaning, you use a product type where the first, let’s say a tuple — a tuple is a product type — so, the first element of the tuple has two cases and the second element has five. Now you’ve got the 2 x 5 and you’ve got 10 again.

This is the kind of thinking that you can do once you realize that there are these product and sum types that you can use.

Why do you want to target the number of cases exactly? I’ve done a very detailed analysis for my book of this. It’s not going to be in the book, because it’s boring.

If you’ve got too many cases in your domain model, that means a case that doesn’t really exist in the real world but it exists in your software, like it’s possible. Let’s say you did 5 plus 6, and so that’s 11.

There’s 11 cases, but one of them shouldn’t be used. At some point, you’re going to either use it or have a conditional to make sure it’s not being used.

Conditionals add complexity to your code. If you have extra cases, you’re adding complexity.

What if you had 9 cases when there’s really 10 cases in your domain? If you only have nine cases in your code, that means that you’re probably going to be overloading one of those cases to make up for it.

You’re probably going to be using one of those cases in two different ways and your code is going to have to have a conditional to figure out which one of the cases it really should be.

In both cases, in both of those situations, you are missing the perfect fit between your domain model and the domain itself, and adding complexity because of it.

An example of something very simple, an almost silly example of a time when you might have a misfit is…and this happens a lot. You have something like you’re reading a sensor, like a thermometer.

Sometimes when you do a read, it doesn’t give you a number. But the API you’re given, doesn’t have a way to not return a number because it’s C or something. The return value is always a number.

What does it do? It overloads zero to be either zero or, oops, I didn’t get a reading when you asked me for it, because the sensor was down, or you’re trying to ask too quickly, or something like that.

It’s overloading zero because there’s this case that doesn’t fit within the type. The type that they chose was int. There’s this extra case that they want to represent and that’s not in the type, so they had to overload one of the values of int with this number.

Now, maybe they could have chosen a better number, something that you’re unlikely to ever read, like negative one million, or something like that.

Even if you had that, you still have a conditional that you’re going to have to deal with. If you want it to convert it to a better type, you’d still have a conditional in there, at least it would be in one place in the conversion code.

These things happen in real life. I have a memory of a system where you could accept both credit card and PayPal. With credit card, they were using Stripe and PayPal has its own ID.

The Stripe’s ID and PayPal’s ID were stored in the same database table. To know if someone was a Stripe customer, it would take that ID and run a regex on it to see if it looked like a Stripe ID. To save it as PayPal, they’d run another regex on it to see if it looked like a PayPal ID.

Everywhere where you had to use this, you wanted to get the ID or figure out what to do with it, you had to run this regex on the field.

Instead, they should have realized that if they had known about product and sum types, they would have realized that this is actually a sum type. Instead of overloading this one field for the two, we should have an either PayPal or Stripe, or some other system like that, that would better fit the domain.

In fact, I have heard that in a recent upgrade, they have made that change because it was causing them a lot of pain. A lot of code which was just checking for, “Is this a Stripe ID or a PayPal ID?” Very duplicative code.

If you have these products and sum types, notice you’ve got plus and times. Product is times and sum is plus. You’re able to target any number that you want, in different ways, in multiple ways.

Like I said at the beginning, you can have all 10 cases enumerated in one type, or you could break it up into two 5s and sum them. Or you break it up into three and seven and sum them. Or you could have two six and another two. There’s all sorts of ways that you can break it up.

That’s what the product and sum type gives you. It gives you this flexibility to really dig deep into the domain and model it both correctly, because you can just do the math and know, yes, these have all the possible cases, and I’m not missing any and I don’t have any extra. You have the flexibility to choose whatever way you want it.

If you want to break it down into two, like I said, two fives, and then sum them, you can do that. You don’t have to have the one 10. If you want to break it down like I said before into 2 and 5 and multiply them, then you can get the 10. You can do two fours and then add a two, two times four and then add a two to get the 10.

Once you open this door, you can see how easy it is to target a specific number of cases and it lets you analyze whether you’re actually getting the right number of cases and avoid the problems of using a product type and not realizing that…

Let’s say you wanted to target nine things. You used a two times five thinking “Oh, that 10th one, I’m never going to use it. It’s going to be fine.”

You don’t realize the problems. What if you did three times four and you had three extra cases?

That’s even worse. You’re adding complexity to your software. You’re creating conditions where a value is possible to be created, but not meaningful in your domain. You’re asking for trouble. If you can avoid it, you might as well avoid it at the beginning.

Just to recap, we have product and sum types, which let you easily model any particular number of cases that you might have. They let you easily analyze how many cases you do have. They’re very useful for having all the flexibility to target particular numbers, and being able to know how many you actually have.

You have both. It’s both easy to create and very flexible, and easy to see how many you have, so in analysis.

All right, if you liked this episode, you should go to lispcast.com/podcast. There you’ll find all the past episodes, including the one where I explain product and sum types and all the other ones with audio, video, and text transcripts.

You’ll also find links to subscribe and to find me on social media. Get in touch. I love getting these questions. I love answering them on the podcast.

My name is Eric Normand. This has been my thought on functional programming. Thank you for listening, and rock on.

The post What do product and sum types have to do with data modeling? appeared first on LispCast.

Permalink

32: Clojure, Kafka, and OPERATR with Derek Troy-West

Derek Troy-West talks about scaling systems with Clojure, Kafka, and building systems with pure data. “I write Clojure almost every day” Troy-West OPERATR OPERATR demo Follow The Data - Derek Troy-West - Clojure/Conj 2019 Verrency Apache Kafka Three Ways

Permalink

Copyright © 2009, Planet Clojure. No rights reserved.
Planet Clojure is maintained by Baishamapayan Ghose.
Clojure and the Clojure logo are Copyright © 2008-2009, Rich Hickey.
Theme by Brajeshwar.