LINUX.ORG.RU

О статической типизации для обычных людей

 ,


0

3

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

Есть три функции, которые должны всегда идти по порядку. То есть func_1(), func_2(), func_3() - правильно, а func_3(), func_2(), func_1() или func_2(), func_1(), func_3() - неправильно. Ошибка должна определяться на этапе компиляции.

Решаем с помощью немного нестандартного чейнинга:

#include <iostream>

using namespace std;

class Chain3
{
private:
    Chain3(){}
public:
    friend class Chain2;
    void func_3()
    {
        cout << "func_3()" <<  endl;
    }
};

class Chain2
{
private:
    Chain3 chain3{};
    Chain2(){}
public:
    friend class Chain1;
    Chain3 & func_2()
    {
        cout << "func_2()" <<  endl;
        return chain3;
    }
};

class Chain1
{
private:
    Chain2 chain2{};
public:
    Chain2 & func_1()
    {
        cout << "func_1()" <<  endl;
        return chain2;
    }
};

int main()
{
    Chain1().func_1().func_2().func_3();
    return 0;
}

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

Кроме типизации тут использованы ещё и особенности языка C++, позволяющие обойтись без макросов. В си или расте, вероятно, без макросов не получится. На Go, вероятно, такой код вообще не написать, хотя там и статическая типизация.

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


Ответ на: комментарий от t184256

Питон будет выполнять звенья, пока не дойдёт до неправильного, на котором упадёт. А если эти три функции будут иметь одинаковые имена, то вообще не заметит. У меня же просто не запустится некорректный код. И тут будет не просто компиляция против интерпретации, а проверка типов.

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

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

Что касается Go, то хотелось бы увидеть реализацию и на нём, конечно.

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

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

Kogrom
() автор топика

На Go, вероятно, такой код вообще не написать, хотя там и статическая типизация.

Если нет требований «каждая функция из цепочки должна вызываться хотя бы раз» и «каждая функция из цепочки должна вызываться не больше раза», то вот так, например:

type Chain1 struct{}
type Chain2 struct{}

func Func1() Chain1 {
	return Chain1{}
}

func (_ Chain1) Func2() Chain2 {
	return Chain2{}
}

func (_ Chain2) Func3() string {
	return "Hello, World"
}

func main() {
	chain1 := Func1()
	chain2 := chain1.Func2()
	result := chain2.Func3()

	fmt.Println(result)
}

Подразумевается, что у типов Chain* приватная реализация, и создавать их можно только через функции Func*.

Кроме типизации тут использованы ещё и особенности языка C++, позволяющие обойтись без макросов. В си или расте, вероятно, без макросов не получится

Потрясающе!

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

Так и в чём преимущество? Я могу дебажить полурабочую реализацию на питоне, где есть ещё какие-то проблемы кроме порядка этих функций, а ты не можешь. Я могу динамически подменять реализации отдельных функций чтобы тестировать, кастомизировать чужой код и обновлять логику игры без перезапуска, а ты нет. Откуда вообще эта вера, что если посильнее дать себе молотком по рукам, побольше запретить прелинтингом/компилятором, то жизнь сразу станет лучше? Единственное для чего это реально нужно это производительность и оптимизации.

neumond
()
Ответ на: комментарий от Kogrom
class Chain3:
  def func_3(self):
    print('func_3()')

class Chain2:
  def __init__(self):
    self._c3 = Chain3()
  def func_2(self):
    print('func_2()')
    return self._c3

class Chain1:
  def __init__(self):
    self._c2 = Chain2()
  def func_1(self):
    print('func_1()')
    return self._c2

Chain1().func_1().func_2().func_3()
Chain1().func_3().func_2().func_1()
func_1()
func_2()
func_3()
Traceback (most recent call last):
  File "/home/neumond/togezzer/kubernetes/chain.py", line 20, in <module>
    Chain1().func_3().func_2().func_1()
AttributeError: 'Chain1' object has no attribute 'func_3'. Did you mean: 'func_1'?
neumond
()
Ответ на: комментарий от neumond

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

Уууу… В одной теме бинго не собралось, решили продолжить в другой.

А я, в свою очередь, хочу точно знать, что заданных видов проблем в программе НЕТ. Чтобы работать только с теми, которые в программе ЕСТЬ.

а ты не можешь.

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

Откуда вообще эта вера, что если посильнее дать себе молотком по рукам, побольше запретить прелинтингом/компилятором, то жизнь сразу станет лучше? Единственное для чего это реально нужно это производительность и оптимизации.

Откуда вообще эта вера, что если в программе кисель, то программировать быстрее и проще. Единственное для чего требуется программный кисель – это если кисель находится в голове у программиста.

wandrien ★★
()
Ответ на: комментарий от t184256
vadim@aquila:/tmp$ grep .  m*.py 
m1.py:class Chain3:
m1.py:  def func_3(self):
m1.py:    print('func_3()')
m1.py:class Chain2:
m1.py:  def __init__(self):
m1.py:    self._c3 = Chain3()
m1.py:  def func_2(self):
m1.py:    print('func_2()')
m1.py:    return self._c3
m1.py:class Chain1:
m1.py:  def __init__(self):
m1.py:    self._c2 = Chain2()
m1.py:  def func_1(self):
m1.py:    print('func_1()')
m1.py:    return self._c2
m2.py:import m1
m2.py:m1.Chain1().func_1().func_2().func_3()
m2.py:m1.Chain1().func_3().func_2().func_1()
vadim@aquila:/tmp$ time mypy m2.py 
m2.py:4: error: "Chain1" has no attribute "func_3"; maybe "func_1"?  [attr-defined]
Found 1 error in 1 file (checked 1 source file)

real	0m0,740s
user	0m0,674s
sys	0m0,062s
Status: 1
vadim@aquila:/tmp$ 

740 миллисекунд на проверку 18 строк кода.

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

То есть func_1(), func_2(), func_3() - правильно, а func_3(), func_2(), func_1() или func_2(), func_1(), func_3() - неправильно. Ошибка должна определяться на этапе компиляции.

void reverse(Chain2& c2, Chain3& c3)
{
	c3.func_3();
	c2.func_2();
	Chain1 c1;
	c1.func_1();
}
 
int main()
{
	Chain1 c1;
	Chain2 c2 = c1.func_1();
	Chain3 c3 = c2.func_2();
	c3.func_3();
	reverse(c2, c3);
	return 0;
}

=>

func_1()
func_2()
func_3()
func_3()
func_2()
func_1()
korvin_ ★★★★★
()
Ответ на: комментарий от neumond

Думаю, на Питоне можно написать код, который упадёт, если порядок будет неверным. Но данный код не соответствует заданным условиям, так как можно напрямую вызвать Chain3 и Chain2. Попробуйте ещё раз.

Kogrom
() автор топика

возвращается ссылка на объект определённого класса

А что если возвращается указатель? Все твои рассуждения идут лесом, потому что в момент компиляции нельзя просто узнать, инициализирован ли указатель.

no-such-file ★★★★★
()

При динамической типизации его не будет.

#lang racket

(module chain racket
  (provide func-1)

  (define (func-1)
    (displayln "func-1")
    func-2)

  (define (func-2)
    (displayln "func-2")
    func-3)

  (define (func-3)
    (displayln "func-3"))

  'end-module:chain)

(require 'chain)

(define (main)
  (define func-2 (func-1))
  (define func-3 (func-2))
  (func-3))

(main)

=>

func-1
func-2
func-3
korvin_ ★★★★★
()
Ответ на: комментарий от no-such-file

А что если возвращается указатель? Все твои рассуждения идут лесом, потому что в момент компиляции нельзя просто узнать, инициализирован ли указатель.

У него возвращаемый объект служит средством тайп-чека, а не хранилищем данных. Он не хранит ничего.

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

в момент компиляции нельзя просто узнать, инициализирован ли указатель

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

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

запрещает создавать неинициализированные экземпляры для данного типа, то можно

Это только верхушка айсберга. Как быть с типами вроде Option<>? Нужен как минимум паттерн-матчинг в языке. А вообще говоря, нормальная алгебраическая система типов как в ML-ях. Собственно эти ML-и и разрабатывались в рамках концепции «доказательства корректности». Т.е. просто статической типизации недостаточно.

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

no-such-file ★★★★★
()
Последнее исправление: no-such-file (всего исправлений: 1)
Ответ на: комментарий от korvin_

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

Kogrom
() автор топика
Ответ на: комментарий от no-such-file

Как быть с типами вроде Option<>? Нужен как минимум паттерн-матчинг в языке.

Нужен.

Но впрочем, в C++ можно запретить засовывание типа внутрь option<> или куда-либо еще. Только инициализация фабрикой.

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

Используется не типизация, а, если не ошибаюсь ФП.

Ошибаешься, но и что?

И такой код упадёт при неверном вызове.

(define (main)
  (define func-2 (func-1))
  ;(define func-3 (func-2))
  (func-3))

=>

func-3: unbound identifier in: func-3

— никакого неверного вызова не произошло. И вообще никакого не произошло. «func-1» не напечаталось.

Однако типизация тут будет ни при чём.

Действительно. При чём тут типизация?

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

будет другой код, с другим смыслом

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

no-such-file ★★★★★
()

Есть три функции, которые должны всегда идти по порядку. То есть func_1(), func_2(), func_3() - правильно, а func_3(), func_2(), func_1() или func_2(), func_1(), func_3() - неправильно. Ошибка должна определяться на этапе компиляции.

Что значит «всегда»?

void main_broken()
{
    std::cout << "Broken" << std::endl;
    Chain1 c1 = Chain1();
    Chain2 c2 = c1.func_1();
    Chain3 c3 = c2.func_2();
                c1.func_1();
                c3.func_3();
                c2.func_2();
                c1.func_1();
    std::cout << std::endl;
}

=>

Broken
func_1()
func_2()
func_1()
func_3()
func_2()
func_1()
korvin_ ★★★★★
()
Ответ на: комментарий от korvin_

Да я с первого раза понял этот дефект. Можно усложнить. Chain1 запретить копировать, остальные звенья перемещать с помощью move. Будет время - попробую вечером написать код.

Kogrom
() автор топика

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

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

А покажете как напрямую вызвать Chain3 и Chain2? И какие заданные условия это нарушает.

UPD: увидел что конструкторы 2/3 приватные. Ну ок, можно. От этого тоже можно защититься разными способами, да хоть создавать анонимный класс прямо в методе func_1, ерунда.

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

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

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

От этого тоже можно защититься разными способами, да хоть создавать анонимный класс прямо в методе func_1, ерунда.

Надо бы определиться и не перескакивать по позициям:

  1. «Кисельным программистам зубило ненужно.»
  2. «А ну и что, в питоне тоже можно сделать зубило, правда из бумаги!»
wandrien ★★
()
Ответ на: комментарий от wandrien

У него возвращаемый объект служит средством тайп-чека, а не хранилищем данных. Он не хранит ничего.

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

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

Ага 739 миллисекунд было ПОТРАЧЕНО на запуск интерпретатора и конПеляцию питонячьего кода в питонячий же байткод. Ведь всем известно, что при измерении скорости разгона машины до 100 км/ч учитывается время разогрева движка зимой…

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

Вообще-то нет. Запуск обычного питона и исполнение этой программы происходит в разы быстрее. Речь именно о mypy.

А по меркам процессора 0.7 секунды это целая вечность.

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

Напишите func_4(), которая в правильном порядке вызовет первые три.

Это будет не совсем то, что надо. Чейнинг используется для встроенных DSL. Можно посмотреть в книжке Мартина Фаулера (Domain Specific Languages). Там должна быть определённая вариативность в цепочке. При этом важно соблюдать определённый порядок. Насколько я понимаю, контроль последовательности реализуют во время исполнения, а не во время компиляции. Так гибче и проще реализовать. Я же тут провёл эксперимент с альтернативой.

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

Да, какую-то небольшую защиту можно сделать. Хотя и она легко обходится, но надо будет всё-таки немного заморочиться.

#include <iostream>

using namespace std;

class Chain3
{
private:
    Chain3(){}
public:
    Chain3(Chain3 const &) = delete;
    void operator=(Chain3 const &x) = delete;
    friend class Chain2;
    void func_3()
    {
        cout << "func_3()" <<  endl;
    }
};

class Chain2
{
private:
    Chain3 chain3{};
    Chain2(){}
public:
    Chain2(Chain2 const &) = delete;
    void operator=(Chain2 const &x) = delete;
    friend class Chain1;
    Chain3 && func_2()
    {
        cout << "func_2()" <<  endl;
        return move(chain3);
    }
};

class Chain1
{
private:
    Chain2 chain2{};
public:
    Chain1(Chain1 const &) = delete;
    void operator=(Chain1 const &x) = delete;
    Chain1(){}

    Chain2 && func_1()
    {
        cout << "func_1()" <<  endl;
        return move(chain2);
    }
};

int main()
{
    //Chain1().func_1().func_2().func_3(); // ok

    Chain1 c1 = Chain1();
    Chain2 c2 = c1.func_1(); // error: use of deleted function 'Chain2::Chain2(const Chain2&)'
    Chain3 c3 = c2.func_2();
                c1.func_1();
                c3.func_3();
                c2.func_2();
                c1.func_1();
    return 0;
}

Kogrom
() автор топика