Closures in plain C.
От: fk0 Россия https://fk0.name
Дата: 14.12.21 12:13
Оценка:
Hello...

Известны ли методы реализации замыканий в стандартном (C17) C?

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

Но чтоб вызвать функцию с заданными аргументами теперь нужно знать
точный прототип (сигнатуру) вызываемой функции, то есть точно знать и
тип возвращаемого значения и типы аргуменов. Значит функция реализующая
условный operator() функционального объекта должна быть реализована
для всех возможных типов возвращаемых значений и всех возможных типов
аргументов. Что, очевидно, ввиду отсутствия в C шаблонов заставляет
ограничиваться каким-то множеством предварительно сгенерированных
функций для какого-то ограниченного множества типов. Допустим, можно
ограничиться только скалярными встроенными типами, включающими в себя
обощённый указатель (void *), так как для типов заранее неизвестных
функции сгенерировать невозможно (нет шаблонов).

Просматривается общая стратегия реализации замыкания примерно следующая:

1) сам функциональный объект хранит указатель на функцию (в данном
случае тип стирается) и массив unions для аргументов (типы
аргументов тоже не сохранятся);

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

3) создание многомерного массива, хранящего указатели на сгенерированные
функции, где индексом для каждого измерения является условный индекс
типа, а сами измерения отображают возвращаемое значение и множество
аргументов функции;

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

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

Генерация функций и прототипов функций может быть осуществлена с помощью
макропроцессора, с помощью известного макроса MAP, отображающего другой
заданный функциональный макрос на множество заданных аргументов
(https://github.com/swansontec/map-macro).

Можно ли в данной схеме что-то упростить, улучшить? Оставясь при этом
в рамках стандартного C, без GNU расширений. Хотя кажется, все они достаточно
бесполезны, кроме может быть __typeof__, который в части случаев заменяется
на _Generic.

Интерес будем считать сугубо академическим. Показать, что в C можно то же
самое, что в языках более высокого уровня.


Зачем это нужно:

Комбинируя в дальнейшем такие функциональные объекты можно привнести
концепцию монад и ленивых вычислений. В дальнейшем это может пригодиться
для реализации стратегии обработки ошибок принятой в других языках,
таких как Visual Basic, Go, C++ и подразумевающей декларативное управление
порядком исполнения (flow control):

1) в Visual Basic возможен переход на заданную метку в случае ошибки
(оператор ON ERROR GOTO);

2) в Go есть оператор defer позволяющий отсрочить выполнение функции;

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

* CppCon 2015: "Declarative Control Flow"

* C++ and Beyond 2012: Systematic Error Handling in C++


Так же известны реализации механизма отложенных вычислений для языка C
(https://gustedt.wordpress.com/2020/12/14/a-defer-mechanism-for-c/), но
они сейчас не реализуются без изменений компилятора.

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

То есть на практическом примере:

{
   void * const p = malloc(25);
   if (!p) goto DEFER0;
   if (false) {
     DEFER1:
       free(p);
       goto DEFER0;
   }

   void * const q = malloc(25);
   if (!q) goto DEFER1;

   if (false) {
     DEFER2:
       free(q);
       goto DEFER1;
   }

   if (mtx_lock(&mut) == thrd_error) goto DEFER2;

   if (false) {
     DEFER3:
       mtx_unlock(&mut);
       goto DEFER2;
   }

   // all resources acquired

   goto DEFER3;
   DEFER0:;
}


Существует возможность привести такой код к следующей форме:

    SCOPE_GUARD
    {
        void * const p = malloc(25);
        DEFER(free, p);

        void * const q = malloc(25);
        DEFER(free, q);

        if (mtx_lock(&mut) == thrd_error)
            return or break;

        DEFER(mtx_unlock, &mut);
    }


Я в курсе, что код представленный на первом примере может быть приведён
к условно-линейной форме с массой меток в конце функции, но такое решение
как бы не ещё хуже, т.к. заставляет "перепрыгивать" с помощью goto через
инициализацию переменных, что в свою очередь заставляет все переменные
выносить вверх функции и инициализировать хоть чем-нибудь, чтоб избавиться
от предупреждений компилятора, и такой подход в целом, на мой взгляд
ещё только увеличивает возможность возникновения ошибок по целой массе разных
причин...
Re: Closures in plain C.
От: kov_serg Россия  
Дата: 14.12.21 12:38
Оценка:
Здравствуйте, fk0, Вы писали:

fk0> Существует возможность привести такой код к следующей форме:


fk0>
fk0>    SCOPE_GUARD
fk0>    {
fk0>        void * const p = malloc(25);
fk0>        DEFER(free, p);

fk0>        void * const q = malloc(25);
fk0>        DEFER(free, q);

fk0>        if (mtx_lock(&mut) == thrd_error)
fk0>            return or break;

fk0>        DEFER(mtx_unlock, &mut);
fk0>    }
fk0>


Что мешает использовать менеджер ресурсов, который будет следить за выделением ресурсов и он же будет их освобождать.
Выглядеть будет примерно так
static void body_code(scope_t *s) {
  void *p, *q; mutex_t m[1];
  p=scope_mem_alloc(s,25);
  if (scope_mutex_lock(s,m)) return;
  q=scope_mem_alloc(s,25);
}
void body(scope_t *s) {
  scope_exec(s,body_code);
}
Отредактировано 14.12.2021 12:42 kov_serg . Предыдущая версия .
Re[2]: Closures in plain C.
От: fk0 Россия https://fk0.name
Дата: 14.12.21 15:17
Оценка:
Здравствуйте, kov_serg, Вы писали:

_>Что мешает использовать менеджер ресурсов, который будет следить за выделением ресурсов и он же будет их освобождать.

_>Выглядеть будет примерно так
_>
_>static void body_code(scope_t *s) {
_>  void *p, *q; mutex_t m[1];
_>  p=scope_mem_alloc(s,25);
_>  if (scope_mutex_lock(s,m)) return;
_>  q=scope_mem_alloc(s,25);
_>}
_>void body(scope_t *s) {
_>  scope_exec(s,body_code);
_>}
_>


Это хорошо работало бы в языках, где есть лямбды. Или даже в обычном (Turbo) паскале,
где есть понятие вложенных функций. А в голом C неудобно, так как логика разбивается
на большое количество функций, где за логикой собственно уже и не уследить. Ведь
для каждого вложенного scope нужна отдельная функция.

Вдогонку, важное забыл. Для лямбды так или иначе важен КОНТЕКСТ. Который она
автоматически захватывает. А в данном случае функция body_code вынуждена
контекст принимать в аргументах. Так программировать в целом крайне неудобно...

В C есть лямбды, но это или бестолковое GCC-расширение добавляющее вложенные
функции (с трамплинами, исполняемым стеком...), либо "Blocks" из clang. Т.е. все
подобного рода расширения, к сожалению, специфические только для одного конкретного
компилятора.
Отредактировано 14.12.2021 15:21 fk0 . Предыдущая версия .
Re[3]: Closures in plain C.
От: kov_serg Россия  
Дата: 14.12.21 15:38
Оценка:
Здравствуйте, fk0, Вы писали:

fk0> Вдогонку, важное забыл. Для лямбды так или иначе важен КОНТЕКСТ. Который она

fk0>автоматически захватывает. А в данном случае функция body_code вынуждена
fk0>контекст принимать в аргументах.
Таки в чем проблема?
typedef struct body_ctx  {
  int size1, size2, res;
} body_ctx;
static void body_code(scope_t *s) {
  void *p, *q; mutex_t m[1]; body_ctx *ctx;
  scope_get_input(s,(void**)&ctx);
  ctx->res=1;
  p=scope_mem_alloc(s,ctx->size1);
  if (scope_mutex_lock(s,m)) return;
  q=scope_mem_alloc(s,ctx->size2);
  ctx->res=0;
}
void body(scope_t *s) {
  body_ctx ctx[1];
  ctx->size1=25;
  ctx->size2=45;
  scope_exec(s,body_code,ctx);
  if (ctx->res) {}
}

fk0> Так программировать в целом крайне неудобно...
На вкус и цвет фломастеры разные, кому надо удобно, есть нормальные высокоуровневые языки например sql.
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.