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
()
Ответ на: комментарий от 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 ★★★
()
Ответ на: комментарий от 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)
Ответ на: комментарий от wandrien

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

MOPKOBKA ★★★★★
() автор топика
Последнее исправление: MOPKOBKA (всего исправлений: 1)
#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 ★★★★★
()
Ответ на: комментарий от MOPKOBKA

Это плюсы получается, Block оттудава вроде. По сишке я чуть позже отвечу =) Если толком сформулирую ответ.

LINUX-ORG-RU ★★★★★
()
Последнее исправление: LINUX-ORG-RU (всего исправлений: 1)
Ответ на: комментарий от LINUX-ORG-RU

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

MOPKOBKA ★★★★★
() автор топика
Последнее исправление: MOPKOBKA (всего исправлений: 4)
Ответ на: комментарий от 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 ★★★★★
()
Ответ на: комментарий от 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)