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

з.ы. лямбды - синтаксический сахар. потому что всегда (с начала времён) были функторы и ими все спокойно обходились. потом придумали модное слово и понеслось.

Лямбды были в программировании задолго до появления C++ и даже Си. Ты просто крайне необразованна.

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

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

Видимо, даже сраная Луна людям намного нужнее, чем рекурсивная по возврату функция.

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

Именно, что необразованна.

Вот, написал на смолтоке (никогда на нем не писал много кода, но выглядит прикольно):

printSum := [ :x |
	make := [ :acc |
		[ :y |
			Transcript
				show: acc;
				show: ' + ';
				show: y;
				cr.
			make value: (acc + y)
		 ]
	].
	make value: x
].

(((printSum value: 10) value: 20) value: 30) value: 40

Выхлоп:

10 + 20
30 + 30
60 + 40

Смолток появился, скорее всего, еще до рождения нашей уважаемой леди

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

Ну, про теорию я, вообще, не говорю. А так, лямбды были в ООП чуть ли не с самого основания - я про это

Анон, ну какая нахрен теория? Лямбды в программировании с 1960 года во всю. Смоллток их оттуда утащил.

hateyoufeel ★★★★★
()

Держи на Haskell

import System.IO.Unsafe

p = unsafePerformIO . putStrLn

f acc x = p ("acc(" <> show acc <> ") + " <> show x)

class SumRes r where
    sumOf :: Integer -> r

instance SumRes () where
    sumOf _ = ()

instance (Show a, Integral a, SumRes r) => SumRes (a -> r) where
    sumOf x acc = f acc x `seq` sumOf (x + toInteger acc)

main = (sumOf 10 20 30 40 :: ()) `seq` return ()

Вывод:

$ ./carrot
acc(10) + 20
acc(30) + 30
acc(60) + 40
hateyoufeel ★★★★★
()
Последнее исправление: hateyoufeel (всего исправлений: 1)

Добавьте к этому Залу Славы еще и эрланг с эликсиром, потому что железяками Iron_Bug вполне может управлять и эрланг, даже без ее ведома.

Эрланг:

-module(test).
-export([print_sum/1]).

print_sum(X) ->
    Make = 
        fun(Rec, Acc) ->
                fun(Y) ->
                        io:format("acc(~p) + ~p~n", [Acc, Y]),
                        Rec(Rec, Acc + Y)
                end
        end,
    Make(Make, X).
                                    
% > (((test:print_sum(10))(20))(30))(40).
% acc(10) + 20
% acc(30) + 30
% acc(60) + 40
% #Fun<test.1.27936908>

Эликсир:

defmodule LORTest do

  def printSum(x) do
    make = fn f, acc ->
      fn y ->
        IO.puts("acc(#{acc}) + #{y}")
        f.(f, acc + y)
      end
    end
    make.(make, x)
  end

end

# iex> LORTest.printSum(10).(20).(30).(40)
# acc(10) + 20
# acc(30) + 30
# acc(60) + 40
# #Function<1.60269029/1 in LORTest.printSum/1>
anonymous
()
Ответ на: комментарий от Iron_Bug

Шаг 1, напиши функцию, которая возвращает саму себя. Желательно что бы это было отраженно в типе, но через auto как в С++ тоже можно.

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

Если в языке X можно выполнить эти шаги, то можно и повторить пример.

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

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

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

Lisp тоже, хотя там есть типы в качестве расширения, и если кто то выразит этот пример через расширение с типами (Typed Racket например), то это будет победа.

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

В статике это труднорешаемо по определению. Нужно использовать вывод типов. С++, окамл, а где еще, так чтобы не вырвиглазно?

Ну, ладно! Я больше хотел напомнить публике о существовании таких замечательных языков, как смолток, эрланг и эликсир. А то, после длительного общения с молодежью стало как-то грустно. Они и книг-то особо не читают. Зачем, когда есть интернет?

Ладно, свое просветительское дело сделал. Посыл твоего сообщения понял.

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

Вывод типов не обязателен, мой второй пример OCaml его не использует.

Они и книг-то особо не читают. Зачем, когда есть интернет?

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

Молодежь иногда чуть ли не как люди до 50 определяются, для меня это наверное люди максимум до 30, но 30 это уже скуф часто так то, школьников и студентов на ЛОРе мало, так что для распространения информации рекомендую зарегистрироваться в TikTok и выпускать видео там. Главное не ЛОР, площадка не та.

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

Шаг 1, напиши функцию, которая возвращает саму себя.

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

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

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

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

А так можно?

x = print_sum(10)(20)(30)(40)

x(100)

x(100)(200)
(100) и (100)(200) добавляются к X в котором уже есть (10)(20)(30(40).

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

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

Так я на OCaml его описал, вот:

type t = int -> t 
Если представить в C синтаксисе что то такое, то выглядело бы так:
// Тип T это функция которая возвращает T
// и принимает 1 аргумент int
type T = T(int); 

применением auto в плюсах тоже снимает вопрос

auto не связан с динамической типизацией, для динамической типизации нужен std::variant<auto>, возможность назначать переменной разные типы.

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

auto не связан с динамической типизацией,

auto связан с выводом типа(я это ж написал), а не с динамической типизацией. тип все равно будет конкретным, просто его не нужно писать явно.

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

rtxtxtrx все же на Go получилось.

alysnix тип выражен почти в точности как я приводил выдуманный пример выше.

type fn func(int) fn

MOPKOBKA ★★★★★
() автор топика
Последнее исправление: MOPKOBKA (всего исправлений: 1)
Ответ на: комментарий от neumond
struct S(Box<dyn FnOnce(i32) -> S>);

fn print_sum(x: i32) -> S {
    fn make(acc: i32) -> S {
        let f = move |y| -> S {
            println!("acc({} + {})", acc, y);
            make(acc + y)
        };
        S(Box::new(f))
    }
    make(x)
}

fn main() {
    print_sum(10).0(20).0(30).0(40);
}

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

unC0Rr ★★★★★
()
const std = @import("std");

const PrintSum = struct {
    acc: i32,

    pub fn call(self: PrintSum, y: i32) PrintSum {
        std.debug.print("acc({}) + {}\n", .{ self.acc, y });
        return PrintSum{ .acc = self.acc + y };
    }
};

fn printSum(x: i32) PrintSum {
    return PrintSum{ .acc = x };
}

pub fn main() void {
    _ = printSum(10).call(20).call(30).call(40);
}
anonymous
()
Ответ на: комментарий от rtxtxtrx

Выключил гц, запустил 100,000,000 итераций, выводя текущее и общее числа аллокаций на каждой 10,000,000 итерации, включил гц и снова сделал замеры — всё освободилось.

; go vet && go run lor.go
alloc (kib)  totalalloc (kib)  
110          110               
234486       234486            
468861       468861            
703236       703236            
937611       937611            
1171986      1171986           
1406362      1406362           
1640737      1640737           
1875112      1875112           
2109488      2109488           
2343863      2343863           
124          2343877           

Код:

func main() {
	w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
	defer w.Flush()

	memstats := func() {
		var m runtime.MemStats
		runtime.ReadMemStats(&m)
		fmt.Fprintf(w, "%v\t%v\t\n", m.Alloc/1024, m.TotalAlloc/1024)
	}

	debug.SetGCPercent(-1)

	fmt.Fprintln(w, "alloc (kib)\ttotalalloc (kib)\t")

	state := Printsum
	for i := 0; i < 100_000_000; i++ {
		if (i % 10_000_000) == 0 {
			memstats()
		}
		state = state(i)
	}

	memstats()
	runtime.GC()
	memstats()
}
kaldeon
()
Последнее исправление: kaldeon (всего исправлений: 3)
Ответ на: комментарий от kaldeon

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

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

Если поставить принты, то судя по этому выхлопу рекурсия всё-таки есть:

acc(2885226666) + 75964
acc(2885302630) + 75965
signal: interrupt
(exit status 1)

То есть число увеличивается, а увеличивается оно за счёт рекурсивных вызовов. make(acc + y) возвращает функцию, которая потом снова делает make(acc + y) и т.д.

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

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

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

kaldeon
()
Последнее исправление: kaldeon (всего исправлений: 1)

На Crystal:

alias Adder = Proc(Int32, Adder)

def make(acc : Int32) : Adder
  return ->(y : Int32) do
    puts("acc(#{acc}) + #{y}")
    make(acc + y)
  end.as(Adder)
end

def print_sum(x : Int32) : Adder
  make(x)
end

print_sum(10).call(20).call(30).call(40)

https://carc.in/#/r/i0i8

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

напиши функцию, которая возвращает саму себя.

куда возвращает? функция - это просто адрес в памяти. аргументы передаются через стек. ничто не мешает вызывать функцию по адресу и читать из стека, если ты знаешь сигнатуру. это всё существовало сто лет назад.

замыкание - это КЗ. а асс - это жопа. а то, что пафосно обзывают «замыканием» некоторые зануды - это обычный функтор. тоже существовал всегда.

ты по-русски смысл своей задачи можешь написать? просто логически. или там нет логики. что должно вычисляться.

Iron_Bug ★★★★★
()

Давайте что-нибудь поинтереснее. Например, «а ваш мозг логически полноценен?».

Например, вы - новый тимлид. В вашем отделе трое: С++ господин (X), js-макака (Y) и гошник (Z).

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

Задача: за три вопроса выяснить, кто из них кто.

Условия:

  1. сотрудники отвечают только фразами «Фиг его знает» и «ХЗ». Что из этого да, а что нет - неизвестно.

  2. один и тот же вопрос можно задать только одному сотруднику.

  3. вопросы должны подразумевать однозначное подтверждение или отрицание в ответе.

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

то, что пафосно обзывают «замыканием» некоторые зануды - это обычный функтор. тоже существовал всегда.

Функтор в C++ это закат солнца вручную (хотя там всё такое, так что для плюсатых норм). Замыкание так называется потому, что лексический контекст автоматически сохраняется, для этого не нужно никаких танцев с бубном. Поэтому замыкания так широко используются везде, где их завезли, а функторы твои были экзотикой. Можно и без замыканий всё то же самое делать на объектах, только это очень уныло, и потом жаба головного мозга развивается.

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

Задать каждому вопросы по всем правилам, не обращая внимания на ответы, а в конце высказать факт, констатируя его, «встаньте по линейке слева на право первый плюсовик, второй дважаскриптер, третий гошник» иначе я вам премиальные которые составляют 90% оклада поделю на ноль. Всё, после того как встанут выдать рабочие задания на день, на этом планёрка закончена.

LINUX-ORG-RU ★★★★★
()