LINUX.ORG.RU

Man or boy 2к25: Ваш статически типизированный ЯП полноценен?

 


0

4

Когда то Кнут придумал тест для ALGOL реализаций, и он известен под именем «Man or boy test». Но там просто локальные функции, не особо интересно.

Предоставляю вам версию для проверки языка программирования, на то, достоен ли он существовать в 21 веке!

Для начала нарушу это правило (у Python динамическая типизация), и покажу Python версию:

def print_sum(x):
  def make(acc):
    def f(y):
      print("acc(%d) + %d" % (acc, y))
      return make (acc + y)
    return f
  return make(x)

print_sum(10)(20)(30)(40)
Вывод
acc(10) + 20
acc(30) + 30
acc(60) + 40

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

Мое повторение на OCaml с rectypes:

let print_sum x =
  let rec f acc = fun y ->
    printf "acc(%d) + %d\n" acc y;
    f (acc + y)
  in 
  f x
  
let () = ignore (print_sum 10 20 30 40)
Типы он вывел сам, но можно и указать вручную:
type t = int -> t 

let print_sum (x : int) : t =
  let rec f (acc : int) : t = fun (y : int) : t ->
    printf "acc(%d) + %d\n" acc y;
    f (acc + y)
  in 
  f x
  
let () = ignore (print_sum 10 20 30 40)

Языки которые смогли реализовать тест на лямбдах/функциях, их система типов и ее записи позволяет строить рекурсивные по возврату лямбды и функции:

Языки у которых получилось, но замыкания были выраженны через структуры, потому что они так представленны в самом языке, это облегачает построение рекурсивных по возврату типов, потому что рекурсии в структурах есть везде:

Языки у которых пока не получилось без дополнительных средств типа классов/структур для обхода проблем с типами:

  • Rust (использование trait)
  • C (некорректная реализация)
  • Zig (использование классов)
  • D (использование делегатов)

Не являсь полностью статически типизированным языком, через свою систему типов смогли выразить пример

★★★★★

Последнее исправление: MOPKOBKA (всего исправлений: 21)
Ответ на: комментарий от arax

При чем тут сложение? Этот тест на взрослость языка. Чтобы его пройти понадобятся: рекурсия по возврату, замыкания и рекурсивные типы. Когда-то и рекурсия была не везде, а ещё относительно недавно массовые кодеры не понимали что такое замыкание, нормальные типы только вчера завезли в мейнстрим.

anonymous
()
Ответ на: комментарий от MOPKOBKA

Что ты там придумал, ты дебил смешной

anonymous
()
Ответ на: комментарий от anonymous

При чем тут сложение? Этот тест на взрослость языка.

Сложение тут наверное действительно не причем. Но за такой код в продакшене, я бы тебя табуреткой убил :)

arax ★★★
()
trait Then {
    fn add(self, y:i64) -> impl Fn()->i64;
}
impl<F:Fn()->i64> Then for F {
    fn add(self, y:i64) -> impl Fn()->i64 {
        println!("acc({}) + {}", self(),  y);
        move || self()+y 
    }
}
fn main() {
    let print_sum = |x|{ move || x };
    let acc = print_sum(10).add(20).add(30);
    println!("acc {}", acc.add(100)());
}

https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=9c5827952628bd855c0b86ccfb55f667

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

zurg
()
Ответ на: комментарий от unC0Rr

Т.е. это просто фолд такой с эдаким списком?

Списка нет. При компиляции создаются отдельные функции для каждого звена цепочки. Аккумулятор передаётся в эти функции по ссылке (в язычке все аргументы функций - ссылки). В транслированном коде на C++ это показано.

Это неинтересно в рамках задачи ТС.

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

Kogrom
()
Ответ на: комментарий от zurg

Но идея «теста» так себе, вся разница сведётся к языкам с поддержкой лямбд и без, плюс минус синтаксические фичи, как вот у окамла.

А ничем другим языки не отличаются.

MOPKOBKA ★★★★★
() автор топика
Ответ на: комментарий от MOPKOBKA

Это ничем не отличается от того, чтобы подложить промежуточную структуру/класс.

Что это невозможно записать без - не проблема.

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

Если же это абстрактный кейс - это игра в аутизм.

wandrien ★★★
()
Ответ на: комментарий от wandrien

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

anonymous
()
Ответ на: комментарий от MOPKOBKA

Ну так вписывай раст в верхнюю колонку, а то необъективно как-то. Если не понял то рекурсия вот в этой строчке move || self()+y т.е. создаём новое замыкание с такими же телом и сигнатурой как и вызванное внутри self(), идентификатор make поскипан за ненадобностью. Плюсовое же решение при этом засчитал, хотя в нём, чисто синтаксически, make сам себя тоже не вызывает. В остальном, твоим же критериям удовлетворяет, никаких вспомогательных структур нет, трейт точно не структура, ну или выкидывай хаскель и жабу из верхнего столбца. Короче, рейтинг твой типичное «крестик трусы»

zurg
()
Ответ на: комментарий от zurg

Ну так вписывай раст в верхнюю колонку, а то необъективно как-то.

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

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

alysnix ★★★
()
Ответ на: комментарий от wandrien

Прописываю пациенту 10 лет brainfuck внутривенно, до просветления. Там тоже можно все что и в другом языке программирования. Про какие то плоскости не начинай, пока не дашь определение. Я на эту проблему натыкался пару раз, этого тебе достаточно для практической плоскости?

MOPKOBKA ★★★★★
() автор топика
Последнее исправление: MOPKOBKA (всего исправлений: 1)
Ответ на: комментарий от zurg

Проблема твоего решения не в том что там нету названного типа, проблема в том что используется абстрактный класс Then. В С++ нету абстрактого класса.

Java пример я засчитал, потому что через используемый там интерфейс, строятся все замыкания, там другая модель замыкания по сравнению с другими языками, тоже самое и в Crystal, но всю выразительную мощь они представляют.

По Haskell моя ошибка, я думал что class связан только с типами, но сейчас почитав понял что он реализует виртуальные функции. Убираю.

MOPKOBKA ★★★★★
() автор топика
Последнее исправление: MOPKOBKA (всего исправлений: 1)
Ответ на: комментарий от wandrien

Если же это абстрактный кейс - это игра в аутизм.

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

Морковка же выше написал что в рамках предложенного теста А ничем другим языки не отличаются..

За рамками предложенного теста они отличаются, например, есть языки по типу кложуры от банана где все - данные. Можно сказать что все языки делятся на гомоиконные и все остальные. И это тоже будет верно.

Obezyan
()
Последнее исправление: Obezyan (всего исправлений: 1)
Ответ на: комментарий от Obezyan

Морковка же выше написал что в рамках предложенного теста А ничем другим языки не отличаются...

Это я с позиции тех, кто говорит что фичу X можно реализовать и на его языке Y если постараться. Если им не важно что предоставляет язык, а важна только возможность, то любые тьюринг-полные языки равны.

Вот возьмем brainfuck, можно сделать там интерпретатор s-exp и будет код как данные. И зачем нужны другие языки?

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

MOPKOBKA ★★★★★
() автор топика
Последнее исправление: MOPKOBKA (всего исправлений: 2)
Ответ на: комментарий от MOPKOBKA

На какую ты проблему натыкался, болезный? Не сумел operator() прописать в крестах?

Зачем тебе ЯП, если на русском еще мысли формулировать не научился?

wandrien ★★★
()
Ответ на: комментарий от wandrien

А тебе зачем переменные? Не смог в <<<++>>++><--<>++>>? Попробуй другую профессию, программирование не для таких нытиков любителей-«синтаксиса» как ты.

MOPKOBKA ★★★★★
() автор топика
Последнее исправление: MOPKOBKA (всего исправлений: 1)

Тред не читал, там bash уже вносили как сильный и могучий язык?

peregrine ★★★★★
()
#include <stdio.h>
#include <stdlib.h>

typedef struct Closure Closure;
typedef Closure *(Func)(Closure *c, int y);
struct Closure
{
        int acc;
        Func *f;
};

Closure *make(int acc);

Closure *mkclosure(int acc, Func *f)
{
        Closure *c;
        c = calloc(1, sizeof(*c));
        c->acc = acc;
        c->f = f;
        return c;
}

Closure *run(Closure *c, int y) {return c->f(c, y);}

Closure *f(Closure *c, int y)
{
        printf("acc(%d) = %d\n", c->acc, y);
        return make(c->acc + y);
}

Closure *make(int acc) {return mkclosure(acc, f);}

Closure *printsum(int x) {return make(x);}

int main(void)
{
        run(run(run(printsum(10), 20), 30), 40);

	return 0;
}
kaldeon
()
Последнее исправление: kaldeon (всего исправлений: 1)
Ответ на: комментарий от MOPKOBKA

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

Именно это я и попытался объяснить своими словами.

Obezyan
()

трейт тоже структура. только из указателей на функцыи!

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

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

Просто как показывает практика, если человек написал на расте хотя бы пару тысяч строк, хейтером он быть перестает.

anonymous
()
Ответ на: комментарий от MOPKOBKA

А тебе зачем переменные?

Попытаюсь объяснить для любителей синтаксической шизофрении в разрезе практической плоскости. Сомневаюсь, что получится.

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

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

А твой «Man or boy test» это и есть абырвалг, когда в буквы научился, а читать не научился. Потому что ты не сформулировал проблему, которую ты решаешь. Если нет проблемы, то и решения быть не может. Или мнимым «решением» выступает что угодно.

wandrien ★★★
()
Последнее исправление: wandrien (всего исправлений: 4)
Ответ на: комментарий от MOPKOBKA

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

(T2) -> T1
(T1, T2) -> T1

что банально есть везде.

Но для этого надо расколдоваться от магии, что a(x)(x)(x)(x) чем-то принципиально отличается от f(f(f(g(x), x), x), x).

wandrien ★★★
()
Последнее исправление: wandrien (всего исправлений: 2)
Ответ на: комментарий от anonymous

можно всё на сишке записать

Записать можно. Но у тебя логика в тексте отсутствует.

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

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

А пока не смог, получается, что и сишка подходит.

wandrien ★★★
()
Последнее исправление: wandrien (всего исправлений: 1)
Ответ на: комментарий от anonymous

Читаем о Trait на doc.rust-lang

Примечание: ... в других языках trait часто называют интерфейсами ...

Если мало, то прошу на godbolt.

MOPKOBKA ★★★★★
() автор топика
Последнее исправление: MOPKOBKA (всего исправлений: 1)
Ответ на: комментарий от wandrien

Смысл статической типизации в том, ...

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

... создания программ, решающих какие-то задачи

Программа из ОП-поста решает задачу.

Потому что ты не сформулировал проблему, которую ты решаешь.

Рекурсивный по возврату тип, уже несколько раз по треду написал.

MOPKOBKA ★★★★★
() автор топика
Ответ на: комментарий от wandrien

Но для этого надо расколдоваться от магии, что a(x)(x)(x)(x) чем-то принципиально отличается от f(f(f(g(x), x), x), x).

Это какая то болезнь? Я и по треду несколько раз писал о том что на brainfuck все можно повторить, и тебе уже писал. Почему ты игнорируешь мои ответы? Тебе не интересно читать что тебе отвечают?

Почему я выбрал brainfuck для своих комментариев? Там реализуется все что угодно, в том числе и статическая проверка, но при этом сам язык состоит из 8 операторов, которые так и ждут когда wandrien и Iron_Bug перестанут видеть магию в переменных, функциях, и начнут программировать на настоящем языке без излишеств которые придумали глупые люди от незнания.

MOPKOBKA ★★★★★
() автор топика
Ответ на: комментарий от kaldeon

С расширениями можно чуть красивее написать код на Си.

#include <stdio.h>
#include <stdlib.h>
#include <Block.h>

#define lambda(arg, body) Block_copy(^arg { body; return (void**)NULL; });
#define call(f, ...) ((void **(^)())(*f))(__VA_ARGS__)
#define let(name, value) void **name = malloc(sizeof(void*)); *name = (void*)value;

int main()
{
  let (print_sum, lambda((int x), {
      let(make, lambda((int acc), {          
          let(f, lambda((int y), {
              printf("acc(%d) + %d\n", acc, y);
              return call(make, acc + y);
            }));
          return f;
        }));
      return call(make, x);
    }));
  
  call(call(call(call(print_sum, 10), 20), 30), 40);
}
$ clang -fblocks main.c -lBlocksRuntime
$ ./a.out 
acc(10) + 20
acc(30) + 30
acc(60) + 40

MOPKOBKA ★★★★★
() автор топика
Последнее исправление: MOPKOBKA (всего исправлений: 2)
Ответ на: комментарий от MOPKOBKA

Читаем о Trait на doc.rust-lang

Примечание: … в других языках trait часто называют интерфейсами …

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

Там есть упоминание о том, что они похожи на интерфейсы, но это не значит, что они являются интерфейсами. Яблоко на колесо тоже похоже с некоторых точек зрения.

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

Есть Trait Objects (dyn Trait) (https://doc.rust-lang.org/rust-by-example/trait/dyn.html). Это отдельная сущность в языке для динамической диспетчеризации. Но в примере zurg она не используется.

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

anonymous
()
Ответ на: комментарий от anonymous

Трейты не являются типами.

А модули не являются типами в OCaml. Такое разделение не должно удивлять.

dyn Trait ... это отдельная сущность в языке для динамической диспетчеризации.

Как и в Ocaml есть отдельная возможность использовать первоклассные модули, которые и есть модули просто предоставляется возможность реализовывать ДД.

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

Тоже самое с интерфейсами.

Там нет никаких структур из указателей, никаких ни абстрактных, ни обычных классов.

Я про структуры из указателей не говорил, Trait без dyn это тоже самое что и dyn Trait с девиртуализацией.

dyn = рантайм, dyn = компилтайм.

Различия c Java есть, но говорить что trait это не интерфейс?

Даже примеры переведенные почти напрямую с Java работать будут: https://godbolt.org/z/4K49s6K7e

MOPKOBKA ★★★★★
() автор топика
Последнее исправление: MOPKOBKA (всего исправлений: 8)
Ответ на: комментарий от MOPKOBKA

LINUX-ORG-RU, и без jit вставок вполне неплохо получилось закостылить, хотя тест этот пример не проходит конечно. Замыкания работают как надо, можно с произвольного места продолжать несколько раз (как в моем примере с lua).

MOPKOBKA ★★★★★
() автор топика
Последнее исправление: MOPKOBKA (всего исправлений: 1)

Typed Racket. Вывод полуавтоматический.

#lang typed/racket

(define-type t (Integer -> t))

(define (print-sum (x : Integer))
  (define (make (acc : Integer)) : t
    (define (f (y : Integer))
      (displayln (format "acc(~a) + ~a" acc y))
      (make (+ acc y)))
    f)
  (make x))

((((print-sum 10) 20) 30) 40)
monk ★★★★★
()

Попробовал реализовать на C# (использовал старый виндовый 4.8). Первый вариант:

    class Program
    {
        static Func<int, dynamic> Make(int acc)
        {
            dynamic f(int y)
            {
                Console.WriteLine("acc({0}) + {1}", acc, y);
                return Make(acc + y);
            };
            return f;
        }

        static Func<int, dynamic> PrintSum(int x)
        {
            return Make(x);
        }

        static void Main()
        {
            PrintSum(10)(20)(30)(40);
            Console.ReadKey(true);
        }
        
    }

Тут используется тип dynamic и ссылка на Microsoft.CSharp. То есть не совсем честно. Но я думаю, что языки-победители что-то подобное используют.

Второй вариант с честной статической типизацией, но не гибкий:

    class Program
    {
        static void routine(ref int acc, int x)
        {
            Console.WriteLine("acc({0}) + {1}", acc, x);
            acc += x;
        }
        static Func<int, Func<int, Func<int, int>>> PrintSum(int x)
        {
            Func<int, Func<int, Func<int, int>>> make1 = x1 =>
            {
                routine(ref x, x1);
                Func<int, Func<int, int>> make2 = x2 =>
                {
                    routine(ref x, x2);
                    Func<int, int> make3 = x3 =>
                    {
                        routine(ref x, x3);
                        return x;
                    };
                    return make3;
                };
                return make2;
            };
            return make1;
        }

        static void Main()
        {
            PrintSum(10)(20)(30)(40);
            Console.ReadKey(true);
        }        
    }
Kogrom
()
Ответ на: комментарий от Kogrom

Интересный пример, не думал что dynamic где то можно использовать как функцию.

То есть не совсем честно. Но я думаю, что языки-победители что-то подобное используют.

Из первого списка не используют.

MOPKOBKA ★★★★★
() автор топика

kotlin

fun printSum(x: Int): (Int) -> Any {
    fun make(acc: Int): (Int) -> Any {
        return fun(y: Int): Any {
            println("acc($acc) + $y")
            return make(acc + y)
        }
    }
    return make(x)
}

fun main() {
    printSum(10)(20)(30)(40)
}
bvn13 ★★★★★
()
Ответ на: комментарий от LINUX-ORG-RU

Это С но с расширением clang, блоков в плюсах нету %) Блоки из Objective-C взяты, но я не знаток языков Apple. Есть еще Objective-C++.

MOPKOBKA ★★★★★
() автор топика
Последнее исправление: MOPKOBKA (всего исправлений: 4)
Ответ на: комментарий от MOPKOBKA

Аааа, легал читы получатся :) Админ на сервере включил noclip.
Ну тогда ответить будет сложнее, блин но я не успеваю пока. Как получится. Но да, прикольно.

LINUX-ORG-RU ★★★★★
()
Ответ на: комментарий от wandrien

Но для этого надо расколдоваться от магии, что a(x)(x)(x)(x) чем-то принципиально отличается от f(f(f(g(x), x), x), x).

А кто такой Хаскел Карри? И откудова произошло «каррирование»?

Eulenspiegel
()
Ответ на: комментарий от MOPKOBKA

Из первого списка не используют.

Как же там функции одного и того же типа один раз возвращают f(x), а другой раз - просто f? Там либо надо использовать частичную динамику, либо делать матрёшку, как у меня во втором примере. Сомневаюсь, что компилятор её неявно создаёт.

Kogrom
()
Ответ на: комментарий от Kogrom

Как же там функции одного и того же типа один раз возвращают f(x), а другой раз - просто f?

В каком именно примере и в каком месте? Возвращается всегда один тип, и по ассемблерному коду видно что никакого dynamic нету, это видно на строках 19-26 ассемблерного вывода, передается либо один указатель на функцию, либо другой: https://godbolt.org/z/6onMGh39M

Если переписать то что на godbolt я сейчас разместил в псевдо-С++, то пример бы выглядел так:

using fn = fn ();

fn get_fn(int n) {
  fn fn_a() {
    return fn_a;
  }

  fn fn_b() {
    return fn_b;
  }
  
  return n > 0 ? fn_a : fn_b; 
}

MOPKOBKA ★★★★★
() автор топика
Последнее исправление: MOPKOBKA (всего исправлений: 3)
Ответ на: комментарий от hateyoufeel

Можно и без типоклассов. Вот, вывод типов автоматический:

import System.IO.Unsafe

newtype T = T (Int -> T)

p = unsafePerformIO . putStrLn

printSum x = make x where
  make acc = T f where
    f y = p ("acc(" <> show acc <> ") + " <> show x) `seq`  make (acc + y)

(T x) ~~ y = x y

l = printSum 10 ~~ 20 ~~ 30 ~~ 40

main = l `seq` return ()
monk ★★★★★
()
Ответ на: комментарий от monk

newtype T это структура обертка для разрешения рекурсии в функции?

Кстати, выше тут пригодился ассемблерный выхлоп от OCaml. И пока я по OCaml читал всякие статьи, видел советы «а что бы лучше понять, посмотрите этот пример на godbolt». Ты как то спрашивал:

Тогда перед Haskell’ом и лиспом тоже ассемблер изучать? Чтобы смотреть, во что раскрываются замыкания и ленивые списки.

Тут пусть и не Haskell, и не Lisp, но близко. Так что теперь могу сказать уверенно что лишним не будет.

MOPKOBKA ★★★★★
() автор топика
Ответ на: комментарий от monk

Вот ещё более читабельный вариант:

import System.IO.Unsafe
import Text.Printf

p s x y = unsafePerformIO $ printf s x y

newtype T = T (Int -> T)

printSum x = make x where
  make acc = T f where
    f y = p "acc(%i) + %i\n" acc x `seq` make (acc + y)

(T x) ~~ y = x y

l = printSum 10 ~~ 20 ~~ 30 ~~ 40

main = l `seq` return ()
monk ★★★★★
()
Ответ на: комментарий от MOPKOBKA

newtype T это структура обертка для разрешения рекурсии в функции?

Это аналог

type 'a t = 'a -> 'a t 

Указываю, что тип T это функция, возвращающая T.

monk ★★★★★
()
Ответ на: комментарий от monk

Так что можешь переводить Haskell в первый список.

Haskell язык хороший. Но вот писать на нём принято через жопу: алгоритмы в типах, свёртка в другую сторону, генераторы текста в установке пакетов.

monk ★★★★★
()
Ответ на: комментарий от monk

Пока не приступал к изучению Haskell, но работающий аналог мне кажется будет вот таким:

type t = | T of (int -> t)
Это оборачивание в структуру (data), поэтому не могу записать Haskell в первый список.

Мое предположение верно, если

(T x) ~~ y = x y
Расшифровывается как
let (~~) t y = 
  match t with
  | T x -> x y

Но твой пример конечно намного красивее чем тот что был ранее, поменяю ссылку на него.

MOPKOBKA ★★★★★
() автор топика
Последнее исправление: MOPKOBKA (всего исправлений: 3)
Для того чтобы оставить комментарий войдите или зарегистрируйтесь.