Или как максимально использовать возможности мастера CS при выполнении рутинных задач

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

Моя работа связана с программированием на низком уровне C для специализированных встраиваемых платформ, и мне это нравится; Тем не менее, инициализация контроллеров экрана и переключение портов вывода после фиксированной задержки - не главное событие моего дня. Он включает в себя много скучного и бесполезного кода без множества вариантов улучшения.

На прошлой неделе я работал над встроенным устройством PIC, простым таймером с блестками графического интерфейса на монохромном дисплее 128x64. В частности, мне нужно было одновременно управлять парой таймеров: пользователь устанавливает 1-минутную задержку, запускает ее, реле щелкает, и свет остается включенным на 60 секунд, прежде чем снова выключиться. Ура.

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

В этой ситуации мой коллега-самоучка проинструктировал меня создать переменную счетчика и уменьшить ее значение в таймере прерывания, вот так:

unsigned long delay;
/* 1 second repeating interrupt */
void timer_interrupt() {
   if (delay > 0)
      delay--;
}
void main() {
    init_interrupts();
    counter = 10;
    while(counter > 0) {
        // Timer is ticking...
    }
    // Timer is done
}

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

Во-вторых, выставлены глобальные переменные. Не делай этого.

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

Очевидно, да. Я погуглил встроенную библиотеку таймера и нашел репозиторий Github, соответствующий описанию. Функциональность есть, есть возможность создать таймер и узнать, истек ли он или нет; хорошо! Как я могу интегрировать это в свой проект? Какие есть зависимости? Только два:

  • Распределение динамической памяти
  • Функция, возвращающая текущее время

Я нахмурился. Немного, но, тем не менее, слишком. Я работаю над очень маленьким 16-битным PIC: ничто в моей прошивке не требует динамической памяти, и я бы предпочел оставить ее такой. Также мне нужно инициализировать библиотеку указателем на функцию, возвращающую текущее время. Какое текущее время? Секунды? Микросекунды? Наносекунды? В библиотеке есть отдельные процедуры для установки различных уровней задержки, но моя система имеет прерывание только на 20 миллисекунд… И мне не нужно беспокоиться о том, чтобы посмотреть в коде, какие форматы времени на самом деле ожидаются при инициализации.

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

Однако какой бы тривиальной ни была процедура, таймеры должны быть проще. Неужели действительно так сложно включить универсальную реализацию? Ответ - по крайней мере, тот, который я нашел - решительное нет. У вас действительно может быть реализация с нулевой зависимостью для таймеров, которая даже не требует времени.

В тот же период я ​​изучал Haskell, функциональное программирование и монады. В частности, я начал понимать Монаду ввода-вывода и то, как она устраняет потребность в побочных эффектах (не нужно понимать это, чтобы продолжить чтение - я еще не уверен, что понимаю).

C предлагает небольшие инструменты для функционального программирования: нет анонимных функций (лямбда-выражений), нет функций первого порядка, рекурсия возможна, но не рекомендуется без механизма хвостовой рекурсии. Однако кто-то может возразить, что настоящее функциональное программирование - это не столько об этом, сколько об избежании побочных эффектов за счет использования только чистых математических функций, и этого можно достичь с помощью любого языка программирования.

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

Представьте, что вы выполняете работу с библиотекой таймера. Вам дается задержка в секундах и просят отслеживать ее - любую произвольную задержку от 10 секунд до 2 часов. Первое, о чем попросит здравомыслящий человек, - это работающие часы.

Но у меня очень небольшой бюджет, и я не собираюсь давать вам часы; У меня есть один на запястье, но я не хочу его снимать. Тем не менее, я даю вам 15-минутную задержку и говорю, что вернусь, чтобы проверить, прошло ли это или нет. Что бы вы сказали?

«Ну, хоть скажи, который час».

Это ключ к проблеме. Вы можете отслеживать время без часов, если вам сообщают время при консультации. Если я покажу, что таймер запускается в 13.00, вы узнаете, истек ли он, когда я вернусь и скажу, что сейчас 13.14.

Я знаю, что это звучит излишне (конечно, мне не нужно проверять время, если я знаю время, да), но на самом деле это имеет большой смысл, если подумать достаточно серьезно. В некотором смысле, это тот же путь, который идет Haskell с монадами ввода-вывода: побочные эффекты возникают, когда есть скрытое состояние, которое изменяется под капотом; чтобы удалить их, я просто собираюсь открыть реальный мир и передать его в качестве аргумента моей теперь чистой функции.

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

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

Каждая функция совершенно чистая, и интеграция проста: всего два файла, generic_timer.c и generic_timer.h, которым требуется 0 подключений к внешним объектам. Их можно использовать везде, от Linux до Embedded.

Например, функция start_timer принимает два параметра: таймер и текущее время. Он сохраняет текущее время внутри таймера и возвращается. Когда я запрашиваю состояние с помощью is_timer_reached, мне нужно снова передать текущее время: разница между последним и начальным временем быстро подсказывает мне, сколько единиц прошло с соответствующим ответом. По сути, это просто прославленная математическая библиотека!

Полное абстрагирование времени дает второе дополнительное преимущество: все таймеры не зависят от единиц измерения. Хотите считать секунды? Просто скоротайте время в секундах. Нужна более мелкая зернистость? Мне не нужно об этом знать, если вы умеете считать в меньших единицах времени. Почти у каждой функции есть currenttime аргумент, который вы можете строить как хотите - реализации наплевать, пока их число растет.

Типичное использование выглядит так:

// Start the timer somewhere
timer = create_timer();
set_timer(timer, 120); // two minutes
start_timer(timer, get_seconds());
while (work) {
// Other operations ...
    if (get_timer_state(timer) == TIMER_PAUSED) {
        memset(string, ' ', 5);
        string[5] = '\0';
    } else if (is_timer_reached(timer, get_seconds())) {
        stop_timer(timer);
        clear_digout_all();
        sprintf(string, "%02d:%02d", 0, 0);
    } else {
        time    = get_time_left(timer, get_seconds());
        minutes  = time / 60;
        seconds = time % 60;
        sprintf(string, "%02d:%02d", minutes, seconds);
    }
// Display the string ...
}

Вывод

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

Такие решения помогают мне убить время, пока я жду возможности опробовать современные языки, такие как Lua или Rust, на микроконтроллерах.

Примечание.

ранее я сказал, что каждая функция совершенно чиста; Это не совсем правда. В моей реализации обработчики таймера generic_timer_t - это просто typedef для указателей структуры таймера. Технически это означает, что вызов функции с одними и теми же аргументами (скажем, адресом, который указывает на ту же структуру, которая используется снова после уничтожения предыдущего таймера), может дать разные результаты. Чтобы иметь должным образом чистую функцию, я должен передать всю структуру таймера со всеми содержащимися данными вместо произвольного типа дескриптора.

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

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