Статии1 юли 2023 г.10 мин четене
Да мечтаеш в код: Представата за перфектния език за програмиране — DreamLang

Въведение
Езиците за програмиране са като музикални инструменти; всеки има свой уникален глас, характер и предназначение. През годините имах възможността да композирам симфонии с много такива инструменти – от гъвкавите акорди на Python и JavaScript до здравите и прецизни нотки на Rust и Go. Опитът беше просветляващ, разкривайки присъщите силни и слаби страни на всеки език.
И все пак, от време на време, се улавях, че копнея за нещо различно. Мечтаех за език, който съчетава простотата и лекотата на научаване на Go, стабилността и безопасността на Rust, гъвкавата динамичност на JavaScript и функционалната елегантност на Haskell и Elixir.
В тази статия целя да представя подробно изложение за DreamLang, черпейки от най-доброто в съществуващите езици и композирайки го в симфония от чисто, ефективно и приятно писане на код.
Корени в Go
Нашето пътешествие в DreamLang започва с Go — език, ценен заради своята прямота и ефективност. Ключов аспект на Go, който го отличава, е малкият му набор от ключови думи – само 25 на брой. Този минималистичен подход намалява когнитивното натоварване на програмиста, правейки езика по-бърз за научаване и по-прост за използване.
Простотата на Go обаче не възпрепятства способността му да се справя със сложни проблеми. Например, помислете за силата на goroutines в Go, които позволяват леко управление на нишки за паралелни задачи. Ето един прост пример за goroutine в действие:
package main
import (
“fmt”
“time”
)
func hello() {
fmt.Println(“Hello, world!”)
}
func main() {
go hello()
time.Sleep(1 * time.Second)
fmt.Println(“main function”)
}
В този код go hello() стартира нова goroutine. След това основната функция изчаква една секунда и отпечатва своето съобщение. Можем да изпълняваме функции паралелно с минимален синтаксис, което допринася за репутацията на Go за простота и мощ.
Enums и съпоставяне на шаблони от Rust
Докато изграждаме DreamLang върху солидната основа на Go, се обръщаме към Rust за следващото си вдъхновение — силата на неговите Enums и съпоставяне на шаблони.
В Rust, за разлика от много други езици, изброяванията, или Enums, могат да съдържат данни във варианти. Тази способност предлага изразителна езикова характеристика, известна като алгебрични типове данни (ADTs), която ни позволява да моделираме данните си по-точно. Разгледайте следния пример в Rust:
enum Message {
Quit,
ChangeColor(i32, i32, i32),
Move { x: i32, y: i32 },
Write(String),
}
Тук Message е enum, който представя различни видове съобщения, които могат да бъдат изпратени в хипотетично приложение. Всеки вариант съдържа различни типове и количества информация, което прави тази структура далеч по-изразителна от това, което предлагат традиционните изброявания в много други езици.
Наред с enums на Rust, съпоставянето на шаблони издига безопасността и изразителността на езика. Съпоставянето на шаблони в Rust работи ръка за ръка с enums, за да предостави силна гаранция, че всички възможности са обработени в кода. Ето пример, който използва съпоставяне на шаблони с нашия предварително дефиниран Message enum:
fn process_message(msg: Message) {
match msg {
Message::Quit => {
println!(“The Quit variant was passed in.”);
}
Message::ChangeColor(r, g, b) => {
println!(“Change the color to red {}, green {}, and blue {}”, r, g, b);
}
Message::Move { x, y: new_name } => {
println!(“Move in the x direction {} and in the y direction {}”, x, new_name);
}
Message::Write(text) => {
println!(“Text message: {}”, text);
}
}
}
По отношение на съпоставянето на шаблони, DreamLang ще запази безопасността и интуитивността на подхода на Rust. Ето един въображаем пример как функцията process_message може да изглежда в DreamLang:
func process_message(msg: Message) {
match msg {
Message.Quit => {
fmt.println(“The Quit variant was passed in.”);
}
Message.ChangeColor(r, g, b) => {
fmt.println(“Change the color to red {}, green {}, and blue {}”, r, g, b);
}
Message.Move { x, y } => {
fmt.println(“Move in the x direction {} and in the y direction {}”, x, y);
}
Message.Write(text) => {
fmt.println(“Text message: {}”, text);
}
_ => {
fmt.println(“Received an unknown message variant.”);
}
}
}
Чрез интегрирането на Enums и съпоставянето на шаблони от Rust, DreamLang цели да предостави на разработчиците инструментите за писане на изразителен, типово-безопасен и интуитивен код, хармонично съчетавайки простотата на Go с богатата изразителност на Rust.
Възприемане на начина за обработка на грешки от Rust
При разработването на DreamLang се обръщаме към други езици за характеристики, които водят до стабилен, поддържаем и ефективен код. Една такава характеристика е подходът на Rust към обработката на грешки, който постига баланс между прозрачност и използваемост.
Rust използва тип Result и оператора ? за управление на грешки. Функции, които могат да се провалят, връщат тип Result, който е enum с варианти Ok(value) за успех и Err(err) за неуспех. Операторът ?, използван след извикване на такава функция, разопакова варианта Ok или незабавно връща варианта Err, ефективно разпространявайки грешката нагоре. Този подход изисква от програмистите да обработват грешките изрично, повишавайки надеждността на кода.
Например, в Rust:
use std::fs::File;
fn open_file(filename: &str) -> std::io::Result<()> {
let f = File::open(filename)?;
Ok(())
}
Тук операторът ? опростява обработката на грешки, като разпространява грешката, ако File::open се провали.
В сравнение с изричната обработка на грешки в Go, където всяка функция, която може да върне грешка, е последвана от проверка if err != nil:
file, err := os.Open(“file.txt”)
if err != nil {
log.Fatal(err)
}
Подходът на Rust минимизира шаблонния код и подобрява четимостта, особено в сценарии с множество извиквания, които могат да върнат грешка. Ето защо го възприемаме в DreamLang. Нека видим как DreamLang може да се справи с отварянето на файл:
import “io”
func open_file(filename: String) -> Error {
let f = io.File.open(filename)?; // this will return error if open fails
let f = io.File.open(filename)?[ErrorType]; // this will return ErrotType and wrap error if open fails
let f = io.File.open(filename)??; // this will painc if open fails
let f = io.File.open(filename)???; // this will abort program if open fails
}
С оператора ? DreamLang съчетава ефективната обработка на грешки на Rust с простотата на Go. Този подход прави обработката на грешки прозрачна, насърчава програмистите да се справят с потенциални грешки и води до стабилен, устойчив код. В DreamLang се стремим не само към лекота и ефективност при писането на програми, но и към сила и устойчивост при обработката на грешки.
Включване на pipe оператора от Elixir
В стремежа си да изградим DreamLang, заемаме pipe оператора на Elixir, друг мощен инструмент, който добавя елегантност и четимост към функционалното програмиране. Pipe операторът |> взема резултата от една операция и го предава като вход на следващата.
В Elixir код като този:
“HELLO”
|> String.downcase()
|> String.reverse()
се чете като поредица от трансформации. Първо, "HELLO" се преобразува в малки букви, след което резултатът от тази операция се обръща. Резултатът от една функция безпроблемно се превръща във вход за следващата, създавайки конвейер от операции, който е лесен за проследяване.
Включването на pipe оператора в DreamLang ще доведе до по-чист код, където трансформациите са лесно проследими, позволявайки плавен поток на данни между функциите. Предвиждаме това да бъде особено полезно в сценарии, включващи трансформации на данни, където една стойност преминава през поредица от промени, преди да достигне окончателната си форма. Тази вдъхновена от Elixir функция цели да направи кода на DreamLang по-четим, поддържаем и изразителен.
В DreamLang pipe операторът ще работи по подобен начин, превръщайки поредица от извиквания на функции в четима последователност от операции. Ето как може да изглежда:
import “strings”
*func transform_and_display(input: String) {
let result = input
|> strings.trim()
|> strings.toLower()
|> strings.split(“ “)
|> strings.join(“-”)
|> strings.reverse();
fmt.println(result);
}*
transform_and_display(“ HELLO WORLD “);
Този по-дълъг конвейер от операции, осъществен от pipe оператора, демонстрира как могат да се композират сложни трансформации по четим и поддържаем начин. Заемайки pipe оператора на Elixir, DreamLang подобрява четимостта на кода и позволява елегантна композиция на функции, което е основен принцип на функционалното програмиране. Това прави DreamLang по-изразителен и ефективен език за работа на програмистите.
Възприемане на композицията на функции и къринг от Haskell
Докато оформяме DreamLang, нашият стремеж да съберем най-добрите програмни характеристики ни отвежда в царството на Haskell — език, известен със стриктното си придържане към принципите на функционалното програмиране. Два съществени аспекта на Haskell, които възприемаме в DreamLang, са композицията на функции и кърингът.
Композицията на функции позволява свързването на функции във верига, където изходът на една функция служи като вход на друга. В Haskell:
let f = negate . abs
f (-5) — This would output 5
Тук negate и abs са композирани, за да образуват нова функция f.
Кърингът, друга мощна концепция, преобразува функция, която приема множество аргументи, в поредица от функции с единични аргументи. В Haskell:
let add = (+)
let addTwo = add 2
addTwo 3 — This would output 5
add е кърирана функция, която приема два аргумента. Като предоставяме един аргумент (2), създаваме нова функция addTwo.
За да подобри четимостта и повторната използваемост на кода, DreamLang въвежда композицията на функции и къринга в своя синтаксис. Ето как изглежда това:
import “math”
// Function Composition
var negate_abs = math.negate . math.abs;
negate_abs(-5); // This would output 5
// Currying
func add(a: Int, b: Int) Int {
return a + b;
}
var addFive = add(5, _);
addFive(3); // This would output 8
Чрез композицията на функции и къринга, DreamLang насърчава модулен и повторно използваем код, подобрявайки четимостта и придържайки се към принципите на функционалното програмиране.
Заемане на поддръжката на JSON от JavaScript
В постоянно развиващия се свят на програмирането, JSON (JavaScript Object Notation) се наложи като лингва франка за обмен на данни. Предвид факта, че е лесен за използване както от хора, така и от машини, той се използва широко в уеб услуги, бази данни и др. Следователно е съвсем естествено DreamLang, стремейки се да бъде практичен и модерен език, да включва безпроблемна поддръжка на JSON.
В JavaScript манипулирането на JSON е изключително интуитивно:
let student = {
“name”: “John”,
“age”: 30,
“city”: “New York”
};
student.age = 40; // Change a property
Тук се създава JSON обект student, а свойствата му могат да бъдат достъпвани и променяни директно.
DreamLang отива стъпка по-далеч, третирайки JSON като вграден тип данни, което позволява директно създаване и манипулиране без нужда от изрично парсване или превръщане в низ:
var student : JSON = {
“name”: “John”,
“age”: 30,
“city”: “New York”
};
student.age = 40; // Change a property
За по-голяма безопасност и предвидимост, DreamLang въвежда силно типизиране за JSON обекти, базирано на JSON Schemas:
type Student struct {
“name”: String,
“age”: Int,
“city”: String
};
var john : Student = “{
“name”: “John”,
“age”: 30,
“city”: “New York”
}”
Този подход, базиран на схема, налага проверка на типовете за JSON свойствата, което води до по-малко грешки по време на изпълнение и по-надежден код. Чрез безпроблемното интегриране на JSON и добавянето на стабилна проверка на типовете, DreamLang улеснява работата с JSON данни, което го прави идеален за съвременни програмни задачи.
Внедряване на макросистемата и метапрограмирането от Rust
Макросистемата и възможностите за метапрограмиране на Rust са впечатляващи инструменти за генериране и манипулиране на код. Те позволяват на разработчиците да дефинират повторно използваеми шаблони на код и да автоматизират задачи, които биха били досадни и податливи на грешки при ръчно изпълнение. Силата на макросите на Rust може да бъде използвана за създаване на езици, специфични за дадена област (DSLs), осигурявайки ниво на абстракция, което подобрява четимостта и поддръжката.
Прост макрос в DreamLang:
macro html {
($($body:tt)*) => {
let mut _html = “<html>\n”;
_html += $($body)*;
_html += “\n</html>”;
_html
};
(head $($body:tt)*) => {
“<head>\n” + $($body)* + “\n</head>\n”
};
(body $($body:tt)*) => {
“<body>\n” + $($body)* + “\n</body>\n”
};
(title $title:expr) => {
“<title>” + $title + “</title>\n”
};
(p $text:expr) => {
“<p>” + $text + “</p>\n”
};
}
var markup = html! {
html! { head html! { title “Hello, DreamLang!” } }
html! { body html! { p “Welcome to the DreamLang macros tutorial.” } }
};
println!(markup);
В този пример на DreamLang имаме макрос html!, който може да генерира различни HTML елементи въз основа на предоставените токени. Конструкциите html!, head, body, title и p допринасят за изграждането на крайния HTML документ. По този начин можем ефективно да създадем DSL за генериране на HTML, което подобрява четимостта и може да направи обработката на такива задачи по-интуитивна в DreamLang.
Това е ключов пример за гъвкавостта и мощта, които една макросистема може да предостави, което я прави ценно допълнение към DreamLang.
Заключение: DreamLang — обединяване на най-доброто от много светове
DreamLang представлява идеалът в дизайна на езици за програмиране, съчетавайки простотата и прагматичния дизайн на Go с мощни характеристики от Rust, Elixir, Haskell и JavaScript. DreamLang предлага стабилността на enums, съпоставянето на шаблони, обработката на грешки и макросите на Rust; pipe оператора на Elixir; композицията на функции и къринга на Haskell; и вградената поддръжка на JSON в JavaScript.
Важно е да се отбележи, че DreamLang поддържа обратна съвместимост с Go, позволявайки на програмистите да използват съществуващи Go библиотеки, като по този начин осигурява богата екосистема от самото начало.
Въпреки че DreamLang остава само концепция, той въплъщава еволюцията на езиците за програмиране — перфектна комбинация от простота, мощ и гъвкавост. Това е визия за бъдещето, в което силните страни на различните езици се обединяват, за да оформят пейзажа на софтуерната разработка.