Начало работы с Frege с использованием Gradle

Дата публикации: 2015-04-01

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

Дело в том, что этот язык является относительно молодым. Да, конечно, его разработка началась ещё в 2011 году, но экосистема до сих пор является не очень продвинутой (хотя в ней есть всё необходимое для комфортной работы, о чём я и хочу рассказать в этом посте). Приятно, что язык базируется на экосистеме JVM и, следовательно, может заимствовать особо удачные решения этой экосистемы — в частности, репозитории Maven и популярные системы сборки Maven, Gradle или Leiningen (для всех перечисленных систем сборки есть соответствующие плагины для работы с Frege).

В частности, сегодня мы рассмотрим интеграцию с Gradle. Для компиляции проекта мы будем использовать frege-gradle-plugin.

Компиляция

Любой проект на Gradle начинается с написания файла build.gradle. Всё, что от нас требуется по сути — это добавление плагина frege-gradle-plugin к определению проекта, а также настройка компиляции получившегося кода на Java (т.к. Frege транслируется в Java). Вот шаблон build.gradle:

buildscript {
  repositories {
    mavenCentral()
  }
  dependencies {
    classpath group: 'org.frege-lang', name: 'frege-gradle-plugin', version: '0.2'
  }
}

apply plugin: 'java'
apply plugin: 'frege'
apply plugin: 'application'

repositories {
  mavenCentral()
}

dependencies {
  compile group: 'org.frege-lang', name: 'frege', version: '3.22.367-g2737683'
}

mainClassName = 'me.fornever.example.Application'

Этот код всего лишь подключает нужные плагины и настраивает компиляцию. Ожидается, что код на Frege будет расположен в каталоге src/main/frege. Соответствие имён каталогов именам Java-пакетов во Frege необязательно, поэтому первый простой модуль можно расположить просто в файле src/main/frege/Application.fr. Для полноты изложения приведу содержимое этого файла (эта простая программа взята непосредственно из документации Frege):

module me.fornever.example.Application where

greeting friend = "Hello, " ++ friend ++ "!"

main args = do
    println (greeting "World")

Чтобы выполнить этот код, нужно запустить в терминале следующую команду:

$ gradle run

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

Для тестирования разработчики Frege и сопутствующих инструментов рекомендуют использовать местную реализацию библиотеки QuickCheck. Далее мы рассмотрим её применение к чистым функциям и к "грязным" вычислениям.

Для работы с QuickCheck достаточно оставить в модуле публично доступные описания т.н. "свойств". Это можно сделать довольно просто:

module me.fornever.example.Application where

greeting friend = "Hello, " ++ friend ++ "!"

main args = do
    println (greeting "World")

import frege.test.QuickCheck

greetingTest :: Property
greetingTest = property $ \f -> greeting f == "Hello, " ++ f ++ "!"

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

$ gradle quickcheck

(Команду quickcheck тоже предоставляет frege-gradle-plugin.)

Поскольку я люблю CI вообще и сервис Travis в частности, то сразу приведу пример .travis.yml для автоматизации запуска таких тестов на сервере интеграции:

language: java
jdk: oraclejdk8
script: gradle quickcheck

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

При этом задача quickcheck не умеет самостоятельно разворачивать монады типа IO, так что, даже если и разместить значения типаIO Property, то это ничем делу не поможет — они не будут протестированы.

Для того, чтобы выполнить "грязные" тесты, придётся реализовать свою исполняющую среду вместо gradle quickcheck (не пугайтесь, это на самом деле совсем просто делается). Начнём с того, что реализуем простой "грязный" тест:

greetingTestIO :: IO Property
greetingTestIO = return greetingTest

Понятно, что в этом тесте на самом деле никакого IO не происходит, но система типов об этом уже ничего не знает. Следующее, что мы сделаем — это дадим возможность передавать mainClassName из командной строки (чтобы можно было запускать не только приложение, но и исполняющую среду для тестов), заменив последнюю строку в build.gradle:

mainClassName = System.getProperty("mainClass") ?: 'me.fornever.example.Application'

Теперь при запуске через gradle run -DmainClass=some.another.Class будет выполнен код другого главного класса. Нам остаётся такой класс написать, и в этом нам поможет исходный код модуля Test.QuickCheckTest.

Видно, что в нём объявлен ряд вспомогательных функций типа quickCheck, quickCheckWith, quickCheckResult, которые принимают Property с разными опциями, а возвращают различные варианты значений, обёрнутых в монаду IO. Получается, что мы можем написать простое приложение, которое использует эти функции для получения результата тестирования, а затем просто возвращает операционной системе соответствующий код (стандартно — 0, если всё выполнилось хорошо, и 1, если какие-то тесты провалились). Сразу приведу всё приложение:

module me.fornever.example.TestApplication where

import frege.test.QuickCheck
import me.fornever.example.Application (greetingTestIO)

checkResult :: Result -> Int
checkResult Success {} = 0
checkResult _ = 1

main :: [String] -> IO Int
main _ = do
    greetingProperty <- greetingTestIO
    result <- quickCheckResult greetingProperty
    return $ checkResult result

Как видно, мне пришлось объявить вспомогательную функцию, которая вычисляет код возврата по результату теста. В случае, если нужно выполнять несколько тестов, можно их результаты любым удобным образом комбинировать в рамках данной функции. Решение не претендует на абсолютную полноту, но в качестве первого шага к тестированию "грязного" кода оно годится. Запускаются тесты следующим образом:

$ gradle run -DmainClass=me.fornever.example.TestApplication

Это же можно легко закодировать в .travis.yml (и тогда Travis будет корректно отслеживать состояние тестов):

language: java
jdk: oraclejdk8
script: gradle run -DmainClass=me.fornever.example.TestApplication

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

Приложение

Код приложения можно найти в проекте frege-example-project.