Войти
ФлеймФорумПрограммирование

Революционные идеи в сфере языков программирования (4 стр)

Страницы: 13 4 5 626 Следующая »
#45
22:47, 5 ноя. 2018

Delfigamer
> В общем, по задумке, этот тред - это не просто помойка для идей, а всё-таки попытка родить нечто прекрасное.
Напишу и я тогда пару мыслей по поводу идеального языка программирования.

Сначала мои требования к идеальному языку:
1) Заменяет все языки — может одновременно быть: системой сборки, произвольным кодогенератором, низкоуровневым ассемблером, компилируемым высокоуровневым и скриптовым языком.
2) Предоставляет полный контроль за преобразованием исходный код → машинный код, т. е. можно вмешиваться в стадии оптимизации, если автоматика недостаточно умная.

Мои идеи, как этого можно добиться:
Язык должен быть, внезапно, интерпретируемым скриптовым, результатом исполнения которого является скомпилированный исполняемый файл.
Все стадии преобразования в машинный код явно реализованы на этом скриптовом языке и могут быть включены стандартным инклюдом.
Т. е. если нужен ассемблер, пишем "use asm.x86", если нужен C-подобный язык — "use C.base" и т. п.
Однако, я не уверен, что подобную схему, в принципе, можно реализовать с приемлемой производительностью.


#46
23:01, 5 ноя. 2018

}:+()___ [Smile]
> Сначала мои требования к идеальному языку
Читаешь мои мысли. Не хватает только пункта:
3) Можно добавлять синтаксический сахар на свой вкус, средствами самого языка.

#47
23:16, 5 ноя. 2018

Great V.
> 3) Можно добавлять синтаксический сахар на свой вкус, средствами самого языка.
Лично я решил, что в построение AST вмешиваться нельзя, т. е. набор операторов и их приоритеты жестко фиксирован.
А вот в рамках построенной AST можно реализовывать любой сахар, какой хочется.

#48
0:26, 6 ноя. 2018

}:+()___ [Smile]
> Лично я решил, что в построение AST вмешиваться нельзя, т. е. набор операторов
> и их приоритеты жестко фиксирован.
Ну хз. Что будешь делать когда захочется писать в стиле sql или html?

#49
1:13, 6 ноя. 2018

Great V.
> Что будешь делать когда захочется писать в стиле sql или html?
Какой то аналог sql/html можно будет реализовать и в рамках стандартного AST, мне этого вполне хватит.
Делать же абсолютно произвольный синтаксис — это путь к бардаку и лапше.

Я, например, предполагаю отказаться от стандартного представления флоатов с целью упрощения правил разбора.
Т. е. вместо 1.23e-4 надо будет писать 1.23*10^(-4) или 1.23/10^4.

#50
6:27, 6 ноя. 2018
Принцип "линии Вирта" кто-нить пробовал? как ощущения?
#51
12:19, 6 ноя. 2018
Изображение

Кажется, я понял, как разобраться с сайд-эффектами.
Итак, базовая семантика.

Операторы. FEF.
В своей основе, вымышленный язык - чисто функциональный.
Базовая единица программы - это оператор. Оператор берёт на вход несколько значений и выдаёт на выходе несколько новых значений. Графически, оператор можно представить в виде ящика.
Операторы - это чистые функции. Всё, что читает оператор, подаётся ему на входы. Всё, что он записывает, снимается с выходов. В частости, если два вызова одного оператора получают одинаковые входы, то выходы так же будут идентичны.
Внутри, оператор - это граф из блоков. Блоки так же ведут себя как чистые функции - берут значения на входе и детерминистски преобразуют их в выходы. Само преобразование определяется типом блока - это может быть как интринсик, встроенный в виртуальную машину, так и обращение к другому оператору. В последнем случае, вычисление блока происходит так, как если бы его заменили на полное содержание соответствующего оператора.
Таким образом, в вымышленном языке, программа - это математическое выражение, а выполнение кода - это последовательное вычисление этого выражения.
Операторы могут рекурсивно обращаться к самим себе - так, в частности, формулируются циклические алгоритмы. Таким образом, во многих программах, их полностью развёрнутое выражение окажется бесконечно глубоким и, в определённых участках, самоподобным; поэтому я называю такую форму "фрактальным выражением" (fractal expression form). Фрактальное выражение, по своим свойствам, так же является и SSA. Отличие от классических вариантов заключается в том, что вместо отдельного потока управления, используются select_if и рекурсия; а роль фи-узлов по большей части выполняют входы операторов.

+ Пример выполнения

А вообще, виртуальная машина не обязана хранить все промежуточные значения в графе. Она может вообще скомпилировать граф в линейную функцию и пользоваться только ей. И вообще, действует as-if rule - ВМ может хоть на ушах стоять и чертей призывать, лишь бы результат совпадал с полученным по базовой семантике.
#52
13:38, 6 ноя. 2018

Объекты.
Вымышленный язык оперирует объектами.
Подобно C++, у объекта есть чётко определённое время жизни. В отличие от C++, у объекта есть чётко определённый хозяин и пользователи.
Параметры операторов обязательно характеризуются тем, что происходит со владением объекта.
Для иллюстрации, представим переменную

local var: type

в таком виде:
std::unique_ptr<type> var;
Возможны следующие виды параметров:
operator f[produce: type] end
f[produce var]
std::unique_ptr<type> f();
var = f();
Это выходной параметр. Внутри, оператор каким-то образом добывает себе объект. По окончании работы оператора, владение этим объектом переходит от оператора к вызывающей стороне.
operator f[consume: type] end
f[consume var]
void f(std::unique_ptr<type>&&);
f(std::move(var));
Вызывающая сторона передаёт объект во владение оператору. У вызывающей стороны остаётся тыква.
operator f[modify: type] end
f[modify var]
std::unique_ptr<type> f(std::unique_ptr<type>&&);
var = f(std::move(var));
Это двусторонний параметр. В начале, вызывающая стороны передаёт объект во владение оператору. По окончании работы, оператор должен возвратить объект вызывающей стороне.
Пока работает оператор, у вызывающей стороны остаётся тыква.
Для описания читающих параметров, введём дополнительный объект:
template<typename T>
class reader_ptr
{
private:
    reader_ptr<T> const* m_parent;
    T* m_object;
    mutable int m_children = 0;
public:
    reader_ptr(std::unique_ptr<T>&& uptr)
    {
        m_parent = nullptr;
        m_object = uptr.release();
    }
    reader_ptr(reader_ptr<T> const& parent)
    {
        m_parent = &parent;
        m_object = parent.m_object;
        parent.m_children += 1;
    }
    reader_ptr(reader_ptr<T>&& other)
    {
        m_parent = other.m_parent; other.m_parent = nullptr;
        m_object = other.m_object; other.m_object = nullptr;
    }
    void close(reader_ptr const& parent)
    {
        assert(m_children == 0 && parent == m_parent);
        parent.m_children -= 1;
        m_parent = nullptr;
        m_object = nullptr;
    }
    std::unique_ptr<T> release()
    {
        assert(m_children == 0 && m_parent == nullptr);
        std::unique_ptr<T> uptr(m_object); m_object = nullptr;
        return uptr;
    }
    ~reader_ptr()
    {
        assert(m_parent == nullptr && m_object == nullptr);
    }
    type operator*() const
    {
        assert(m_object != nullptr);
        return *m_object;
    }
};
На объекта можно повесить читающую ссылку. В отличие от владельца, читателей может быть множество. Пока на объекте висит хотя бы один читатель - его владение нельзя передавать, что означает невозможность изменить его.
В отличие от С++, читающие ссылки так же обладают "личностью" и временем жизни. Ссылка рождается либо от владеющей переменной, либо от другой такой же ссылки. Читающую ссылку нельзя просто уничтожить - её обязательно нужно вернуть к родительской ссылке/переменной. Это позволяет дать гарантию, что после закрытия ссылки у неё не останется висящих производных.
То есть, reader/writer lock - это одна из деталей базовой семантики языка.
operator f[external: type] end
operator f[: type] end -- equivalent: `external` is the default
f[var]
void f(reader_ptr<type> const&);

{
    reader_ptr<type> reader(std::move(var));
    f(reader);
    var = reader.release();
}
// or, when `var` is itself a reader:
{
    reader_ptr<type> child_reader(var);
    f(child_reader);
    child_reader.close(var);
}
Это читающий входной параметр. Оператору даётся читающая ссылка, которую по окончании работы он должен вернуть наверх.
operator f[local: type] end
f[var]
void f(std::unique_ptr<type>);
f(new type(*var));
Это копирующий входной параметр. Вызывающая сторона копирует объект, копия передаётся оператору во владение и у него и остаётся. Копию можно сделать как от объекта под владением, так и от читающей ссылки.

Переменные оператора и поля объекта так же могут быть владеющими или читающими. Например, возможна такая ситуация:

class ListIterator
    local index: Integer
    external target: List
end

procedure f[external list: List]
    for local iterator -> iter: ListIterator in list do
        print[iter.target is list] -- "true"
    end
end


external-ссылки учитываются статически, в процессе создания функции.
_

С точки зрения FEF, такая семантика проявляется в том, что некоторые блоки могут разрушать объекты на своих входах, что накладывает некоторые ограничения на порядок вычисления блоков и, в частности, делает выборочное исполнение select_if-веток обязательной возможностью ВМ.

#53
15:21, 6 ноя. 2018

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

operator thread_body[consume pipe: LowerPipe[Int32, Pair[Int32, Int32]]]
    local total: Int32 = 0
    local dummy: Int32 = 0
    while true do
        with local i from read[modify pipe] do
            total += i
        on : EmptyFailure do
            dummy += 1
        on : TerminateRequest do
            return
        end
        write[modify pipe, new Pair {total, dummy}]
    end
end

local thread: UpperPipe[Pair[Int32, Int32], Int32] = start_thread[thread_body]
for in range[1000] do
    with local p from block_read[modify thread] do
        print[p.first]
        write[modify thread, p.second]
    end
end
terminate[consume thread]


Собственно, базовая семантика работает таким образом.
start_thread берёт лямбду единственным аргументом и создаёт новый поток. Поскольку в базовой семантике все функции - чистые, то сам по себе новый тред полностью изолирован от внешнего мира. Для связи, дополнительно создаётся "объектная труба" - по сути, две очереди объектов. "Нижний" конец трубы передаётся аргументов в лямбду, "верхний" - возвращается результатом из start_thread.
"Верхний" конец трубы ассоциируется с потоком. С точки зрения вызывающей стороны, write в верхний конец кладёт объекты в очередь, тогда как read из верхнего конца продвигает лямбду и может вернуть объект с того конца.
"Нижний" конец трубы ассоциируется с окружением. С точки зрения потока, write в нижний конец так же кладёт объект в очередь, а read из нижнего конца - продвигает окружение.
Таким образом, согласно базовой семантике, сами по себе потоки стоят на месте и ничего не делают. Единственный момент, когда поток включается - это во время вычисления read. С точки зрения самого потока ситуация аналогичная - окружение стоит на месте и не меняется, пока не его не попросят. В каком-то смысле, read из нижнего конца - это явная команда на разморозку всего остального мира. Конечно же, по завершению read, мир снова заморожен в ожидании.
В действительности же, разрешается и даже поощряется гонять потоки параллельно.
Сами по себе read и write синхонизируются по-минимуму - гарантируется только, что read из одного конца трубы увидит объекты в том же порядке, в каком их засунул write с другого конца. read не ждёт на пустой очереди, для этого есть block_read. read и write на одном конце трубы не синхронизируются друг относительно друга, хотя, разумеется, write значения, явно зависящего от результата read, произойдёт позже по времени.
#54
16:46, 6 ноя. 2018

}:+()___ [Smile]
> Делать же абсолютно произвольный синтаксис — это путь к бардаку и лапше.
Наговнокодить можно при любом синтаксисе. Не аргумент.

#55
17:33, 6 ноя. 2018

Актёры. Координатор.
В принципе, описанных средств уже достаточно, чтобы из них составить параллельную систему с общими элементами.
В качестве примера рассмотрим терминал - единый объект console, на который можно сделать print.
Прежде всего, print изменяет состояние терминала (переписывает буфер). Следовательно, требуемая сигнатура - это print[modify console, ...].
Даже если на минуту представить, что мы обманули систему и прописали print как читающую функцию - тогда у функции не останется никаких результатов, и каждый вызов скомпилируется в FEF в блок, который никак не участвует в образовании результатов - что, согласно базовой семантике, является мёртвым кодом, и при первом же преобразовании этот вызов рискует бесследно исчезнуть из программы.
Поскольку язык функциональный - чтобы сделать modify console, придётся в каждую функцию завозить текущий console, а затем новое состояние забирать с результатов. Пока программа работает в один поток - всё в порядке (C99 и C++98 так и делали, говорят, брат выжил), осталось только научить компилятор автоматически продёргивать console вдобавок к явным аргументам и будет ништяк.
С описанной моделью многопоточности, однако, такой фокус не прокатит. Проблема опять состоит в том, чтобы сделать modify console - потоков может быть сколько угодно, но владелец в любой момент времени должен быть один. Единственный способ передать владение - объектные трубы - не подходит для этой задачи, потому что в таком случае, по этому объекту все потоки выстроятся в одну длинную линию и язык окажется бесполезным.
Вот это и есть "проблема сайд-эффектов".
Собственно, моё решение выглядит так. local console сидит в какой-нибудь функции верхнего уровня и никуда оттуда не уходит. Эта функция вызывает основной код через start_thread и затем в цикле слушает трубу на предмет запросов. Наконец, когда основной код хочет чего-либо напечатать - он делает print[modify lower_pipe, ...], который, в свою очередь, делает write[modify lower_pipe, new PrintRequest {...}]. Верхняя функция ловит этот запрос, выводит его в свой console и продолжает вращение в цикле.
Итак, обобщение этой концепции - это координатор - специально написанный оператор, предоставляемый стандартной библиотекой, который крутится в цикле и ворочает запросами. Координатор выглядит приблизительно так:

const Request = closure type [modify state: CoordinatorState]

class CoordinatorState
    local threads: Map[Id, ObjectPipe[Request]]
    local objects: Map[Id, Value]
    local upper: ObjectPipe[Request]
    local terminate: Boolean = false
end

operator coordinator_body[consume pipe: ObjectPipe[Request]]
    local state = new CoordinatorState {upper -> consume pipe}
    while not state.terminate do
        local pending: Worklist[Request]
        for local thread in state.threads do
            with local request from read[thread] do
                push[pending, request] -- can't modify state from inside a for loop
            end
        end
        for local request in modify pending do
            invoke[consume request, modify state]
        end
        with local request from read[state.upper] do
            invoke[consume request, modify state]
        end
    end
end

operator create_thread[f: operator type [] requires environment]: Id requires environment
    operator g[consume pipe: ObjectPipe[Request]]
        invoke[f, environment -> create_environment[consume pipe]]
    end
    write[
        modify current environment,
        consume closure [modify state: CoordinatorState]
            insert[modify state.threads, start_thread_bare[]]
        end]
end

operator send_to_thread[target: Id, rq: Request] requires environment
    write[
        modify current environment,
        consume closure [modify state: CoordinatorState]
            with local thread from get[state.threads, target] do
                write[modify thread, rq]
            else
                write[modify state.upper, consume current closure]
            end
        end]
end


Координатор позволяет делать две вещи.
Во-первых - он позволяет обмениваться информацией нескольким равноправным потокам.
Во-вторых - он позволяет хранить множество объектов, доступных для чтения и записи одновременно нескольким потокам, посредством отправки запросов и получения ответов. Эти объекты в данном языке носят особое название "актёры", и для работы с ними предоставляется дополнительный синтаксис, частью которого является и current environment.
#56
6:23, 7 ноя. 2018

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

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

operator start_fiber[
    body: operator type [
        consume environment: Fiber,
    ],
]: Fiber

Реквесты сразу включают в себя встроенный механизм для ответов - по сути, являются промисами.
-- sender
operator send_request[
    modify : Fiber,
    consume data: Value,
]: RequestHandle

operator is_ready[
    external handle: RequestHandle,
    modify : Thread,
]: Testable

operator receive_response[
    consume handle: RequestHandle,
    modify : Thread,
]: Value

-- receiver
operator listen[
    modify : Thread,
]: Optional[Pair[Request, Value]]

operator send_response[
    consume : Request,
    consume data: Value,
]


Вместо write, для отправки данных используется комбинация send_request+receive_response. send_request кладёт запрос в трубу и тут же возвращает управление. receive_response "размораживает" файбер и получает подтверждение.
send_request не выстраиваются в единый порядок - если отправить два send_request подряд, второй может быть обработан и отвечен раньше, чем первый. Если же send_request стоит после receive_response - это создаёт порядок между ними, и второй запрос обязательно будет отправлен только после получения ответа на первый, даже если между ними нет зависимостей помимо fiber-объекта.
По идее, такие реквесты можно реализовать в виде непосредственных acquire/release-операций над самими объектами.
Кроме того, такие реквесты позволяют без костылей передавать external-ссылки между файберами - send_request забирает ссылку, RequestHandle её хранит, response возвращает обратно.

#57
12:57, 9 ноя. 2018

Жизнь программы выглядит примерно таким образом.

0. Кто-то делает запрос на загрузку модуля. Это может быть import, это может быть адд-он в language, это может быть сама ОС.

operator load_module[name: ModuleName]: Checkable[Module] requires current context

1. Рантайм находит модуль по имени. Если есть уже загруженный инстанс в кэше рантайма - может тут же вернуть его. Если находит файл - пытается его загрузить. Десериализация байткода и компиляция исходного текста - это частные случаи "загрузки". Загрузка может закончиться неудачей - тогда запрос вместо модуля возвращает ошибку.

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

#!shebang gets ignored, I suppose
-- certain comments are also allowed before `language`

language base_parser_name {
    extension_1_name,
    extension_2_name, -- the last comma is allowed, but not mandatory
}
-- if language name is not followed by a `{` brace, it's identical to an empty list `{}`
language base_parser_name

Парсер и расширения так же являются модулями. После прочтения прелюдии, рантайм рекурсивно пытается загрузить модуль base_parser_name.syntax. Этот модуль описывает базовый синтаксис.
concept Parser
    requires operator parse[consume state: current class, consume input: ByteInput]: Checkable[SyntaxNode] requires current context
end

operator create_parser[]: Checkable[Parser] requires current context


Парсер может быть расширяемым - в таком случае, модуль описывает свой подкласс SpecificParser, предоставляющий методы, дополнительно настраивающие синтаксис.
После создания объекта парсера, рантайм по очереди загружает модули extension_n_name.syntax. Эти модули дополняют переданный им парсер своими правилами.
operator register_extension[modify state: SpecificParser]: Checkable[] requires current context

Если в ходе загрузки языка возникнет цикл - это уф, генерируется ошибка. Если какой-то из модулей не экспортирует правильные операторы - это ошибка. Если тип парсера в register_extension не сходится с типом от create_parser - это ошибка.
Заметьте, что расширения не могут корёжить синтаксис как угодно - новые правила могут вставляться только там, где этого допускает базовый язык. Он же проверяет свои сборные правила на предмет противоречий, и может выдать ошибку, если по-хорошему зарегистрировать расширения не получается.
Расширения синтаксиса играют роль макросов.
Для стандартного парсера, библиотека предоставляет специализированный язык для syntax-модулей, в котором расширения записываются в виде BNF-подобных выражений.

3. Если рантайму удалось собрать парсер со всеми причиндалами - он приводится в действие. Парсеру передаётся поток байтов сразу после закрывающей скобки прелюдии. Парсер возвращает абстрактное синтаксическое дерево, составленное из записей такого вида:

class SyntaxNode
    local head: Symbol
    local attributes: Map[Symbol, SyntaxNode]
    local items: List[SyntaxNode]
    local payload: Value
end

Пример дерева приведён в ИТТ #39.

4. Следующий шаг - это семантический анализатор.
Большинство узлов в стандартном языке - это выражения,

concept ExpressionSemantics
    requires operator static_read[modify context: LexicalContext]: StaticValue requires current context
    requires operator static_write[modify context: LexicalContext, external value: StaticValue] requires current context
    requires operator static_execute[modify context: LexicalContext] requires current context
    requires operator build_read[modify context: LexicalContext, modify program: ProgramGraph]: ValueHandle requires current context
    requires operator build_write[modify context: LexicalContext, modify program: ProgramGraph, external value: ValueHandle] requires current context
    requires operator build_execute[modify context: LexicalContext, modify program: ProgramGraph] requires current context
end

Рантайм держит словарь, где каждому имени узла соответствует некоторый объект.
При загрузке модуля, рантайм берёт из этого словаря обработчик корневого узла. В результате его обработки, на выходе должна получится функция инициализации.
concept RootNodeSemantics
    requires operator build_root[
        consume root: SyntaxNode
    ]: Checkable[
        operator type []: Checkable[Module] requires current context
    ] requires current context
end

5. Рантайм вызывает эту функцию - код модуля исполняется. В процессе исполнения, инстанцируются и регистрируются в контексте модуля все его объявления - операторы, типы, переменные и прочие.
После компиляции модуля, тела внутренних функций остаются в виде синтаксического дерева. Функция превращается в байт-код в момент исполнения выражения "operator ... end". Статический анализ так же откладывается до этого момента.

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

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

#58
3:05, 11 ноя. 2018

Значение. Поведение. Лейаут.
В вымышленном языке, 'объект' разделяется на несколько изолированных абстракций.
Значение - это та сущность, которая передаётся между блоками FEF.
Базовую семантику можно проиллюстрировать таким образом:

+ Показать

Собственно, значение - это тот void*, который передаётся между блоками.
Основной инструмент типизации в языке - это поведение. На более физическом уровне, поведение - это массив указателей на функции.
В отличие от виртуальной таблицы, поведение не входит в состав объекта, а является частью контекста. Это позволяет определять поведение сразу для нескольких логически связанных объектов.
Язык предоставляет свой аналог ADL - при вызове функции просматривается не только текущий лексический контекст, но также и все поведения, имеющиеся в контексте и применимые к аргументам.
Основной способ, которым поведение попадает в контекст - это requires behavior-параметры. При переходе к базовой семантике, каждый requires behavior транслируется в дополнительный параметр, через который передаётся экземпляр поведения, соответствующий актуальным типам явных параметров.
Лейаут - это набор функций, похожий на поведение, который предоставляет доступ к полям класса. Чтение и запись полей - конструкция вида object.field - транслируется в вызов функции лейаута, соответствующей этому полю. Лейауты так же транслируются в отдельные параметры, передаваемые вместе со значением объекта.
Лейаут - это абстракция, разделяющая структуру объекта и его физическое представление.
По умолчанию, рантайм автоматически генерирует лейаут для переменной, исходя из рекомендаций оптимизатора и хинтов в атрибутах. Также программисту даётся возможность предоставить свой собственный лейаут, который даст явное представление одного объекта как функции от другого. В предельном случае, создание лейаута относительно двоичных типов - Int32, ByteString, ByteBuffer[24] и прочих - позволяет полностью контроллировать двоичное представление объекта, что будет особенно полезно для взаимодействия с программами на других языках, которые не предоставляют таких возможностей.
Класс, во многих аспектах, идентичен поведению одного параметра. В дополнение к поведению, класс также содержит объявления полей - local- и external-конструкции. При использовании, язык предоставляет доступ к объявленным полям согласно их статическому типу (который включает поведения и лейауты для каждого поля). При объявлении, эти поля становятся требованиями к лейауту - для автоматического, язык сгенерирует лейаут, способный сохранить все объявленные поля без обозримого наложения, для явного, он статически удостоверится в его достаточности.
#59
17:27, 15 ноя. 2018

Обработка ошибок.
В языке, вместо исключений, присущих императивным языкам, для сообщений о неудачах используется специальное семейство типов, называемых "checkable".
Если какая-то функция может потерпеть неудачу - то её результат заворачивается в специальный контейнер, который представляет своего рода юнион успешного результата и возможных вариантов ошибок.
Поскольку язык избегает понятий "критических ошибок" и "неопределённого поведения", то извлечение успешного результата из контейнера нельзя сделать в виде обычной функции - ведь, если в контенере хранится ошибка, то извлечение успешного результата не имеет смысла.
Поэтому, разбор структуры происходит путём передачи лямбды в метод контейнера, которая получает результат аргументом в случае успеха.
В C++, такая идиома выглядит следующим образом:

template<typename T>
struct PossibleValue
{
    union
    {
        T success_result;
        Failure failure_result;
    };
    bool is_success;
};

template<typename T, typename F1, typename F2>
void with_do(PossibleValue<T> self, F1 if_success, F2 if_failure)
{
    if (self.is_success)
        if_success(std::move(self.success_result));
    else
        if_failure(std::move(self.failure_result));
}

template<typename Dom, typename Cod>
PossibleValue<Cod> Map<Dom, Cod>::get(Dom const& key) const
{
    Cod* ptr = this->internal_get_value_ptr(key);
    if (ptr)
        return make_success<Cod>(*ptr);
    else
        return make_failure<Cod>(MissingKeyFailure{});
}

void f(Map<int, char const*> const& map)
{
    with_do(
        map.get(1020),
        [](char const* value)
        {
            printf("%s\n", value);
        },
        [](Failure const& e)
        {
            printf("%s\n", e.what());
        });
}
В описываемом языке, для работы с такими объектами вводится дополнительная синтаксическая конструкция:
with <parameter-list> from <source-expression> do
    <normal-expression-sequence>
on <local-name-1> : <failure-type-1> do
    <specific-handler-expression-sequence-1>
on <local-name-2> : <failure-type-2> do
    <specific-handler-expression-sequence-2>
else
    <general-handler-expression-sequence>
end

expression-sequence впоследствии передаются методам объекта, возвращаемого source-expression, в виде функций. В случае соответствия, внутренний код объекта вызывает эту функцию, передавая ей действительные значения соответствующего типа.
Главная особенность состоит в том, что такая схема позволяет полностью избежать "висящих переменных" - само создание переменной только при условии валидности запроса. При альтернативных подходах, либо существуют ситуации, когда переменная может быть невалидной:
char const* str = nullptr;
// str is invalid
if (try_get(map, 1020, str)) {
    puts(str);
}
// str may be invalid
либо существует возможность неопределённого поведения:
if (exists(map, 1020)) {
    char const* str = get(map, 1020);
    puts(str);
} else {
    char const* str = get(map, 1020);
    // str is what??
}
либо происходит процедурный бедлам с исключениями и longjmp, которые очень плохо вписываются в функциональную природу вымышленного языка.

Кроме того, такая семантика позволяет пойти ещё дальше. Раз "висящие переменные" в коде нигде не нужны, то можно усилить саму семантику языка и утвердить правило, что "висящие переменные" невозможны. Если к переменной можно обратиться - значит, она гарантированно обладает валидным значением.
Соответственно, если некая функция объявлена с возвращаемым типом Stuff - значит, какие бы входные данные в неё не поступили, в каком бы состоянии не находилось окружение, эта функция в любом случае вернёт валидный Stuff. И напротив - если в некоторых случаях функция не может вернуть осмысленный результат, то это будет явно указано в прототипе формулировкой Optional[Stuff].

Страницы: 13 4 5 626 Следующая »
ФлеймФорумПрограммирование