Всем привет!
В этой статье я хочу рассказать о том, как учеба в (моём случае техническом) ВУЗе может повлиять на амбиции и подход к своему хобби, при этом не всегда имея прямую связь. В пример буду ставить свой проект: приложение для моделирования физических процессов.
Введение
Я считаю, верным подходом будет сначала заглянуть в прошлое и рассказать о том, как до этого я подходил к программированию.
До поступления на свою специальность я писал совсем непримечательные приложения, такие как всяческие списки дел, простые игры и скрипты. Мне нравился процесс, но по сути на тот момент он не выходил за шаблон. Я писал те программы, решения к которым уже были известны давно и всё их написание сводилось к тем же самым решениям.
Охарактеризовать это я могу как обычное хобби сравнимое с обычной игрой на гитаре, к примеру. Я имею в виду, что когда ты играешь для себя тебе необязательно стремиться к чему‑то новому и совершенствовать свою игру, можно играть одни и те же песни и получать удовольствие. Такой подход имеет место быть, ведь главный смысл хобби это делать то, что приносит тебе радость.
Но всё сильно изменилось, когда я поступил на первый курс специальности, связанной с технологией ядерного топливного цикла. Тогда я постепенно начал осознавать, что любой продукт за собой несёт не просто идею и желание сделать это, а целый ряд трудоёмких действий. Стало понятно: чтобы воплотить свою задумку в жизнь нужно пройти серьёзный мыслительный процесс.
Грубо говоря можно его разбить на несколько пунктов:
-
Идея. С неё все и начинается, как ни странно
-
Планирование и совершенствование. Под этим пунктом я подразумеваю превращение простой идеи в жизнеспособную реализацию на бумаге и обдумывание того, что в конечном продукте должно быть, чтобы всё это действительно можно было воплотить в жизнь
-
Воплощение идеи в жизнь. На данном этапе и появляется тот продукт, который когда‑то был просто мыслью.
Конечно, это очень грубое упрощение. В каждый пункт можно добавить несколько подпунктов, но здесь мы будем придерживаться и такой простой модели.
Дальнейшее повествование я и разобью на эти пункты.
Пункт первый: Идея
На первом курсе у меня было множество разных дисциплин: физика, неорганическая химия, инженерная графика и другие. Тогда я начал восхищаться тем, как человек смог добиться такого понимания мира и серьёзно заинтересовался техническим подходом. В тот момент возник вопрос: что я могу сделать, имея такие знания? В голову пришла мысль: написать собственное приложение для моделирования физических процессов. Да, задача крайне амбициозная, но такие амбиции требуют соответствующей им организации.
Первоначально идея звучала так: написать программу, где пользователь сможет заносить нужные формулы, проводить с ними нужные действия и смотреть на результат в виде графика или численных значений для начала.
Идея, конечно, тоже не нова, но гораздо более уникальная, чем то, что я писал до этого. Тогда уже было ясно: будет множество моментов, которые требует разработки и доработки, чтобы это можно было реализовать, поэтому за этим последовало…
Пункт второй: Планирование и совершенствование
Планирование архитектуры
Первое, что встало для меня на пути: а как мне следует это реализовывать? То есть какая архитектура будет у программы. И с этого момента всегда начинается выбор наиболее подходящего варианта, ведь любое решение имеет свои плюсы и минусы.
Здесь стояло на выбор пара вариантов: писать монолитное приложение или вынести разные компоненты приложения в отдельные библиотеки. Тут уже можно сказать о том, что оба этих варианта имеют свои преимущества. Например, если выбрать монолитное приложение, то разработка будет сама по себе проще, так как все лежит в одном месте, да и тестировать такое будет легче. Но это только по началу, а затем можно будет столкнуться с множеством проблем: от сложности расширения, до полного бардака в реализации. Второй вариант, который я и выбрал имеет сразу несколько преимуществ: я могу отдельно писать куски логики и уже получать первые её версии, а также простота интегрирования одной части в другую. К тому же я смотрел на данный продукт, как тот, который будет очень часто обновляться и далеко не всегда равномерно, что теоретически позволило бы пользователю получить маленький апдейт только одной части программы и не тянуть за собой еще кучу всего.
То есть у нас есть изначальная библиотека, которая умеет парсить и считать математически выражения и функции. От неё зависят: библиотека для матанализа и физическая библиотека. При этом они обе могут использоваться друг в друге. Затем уже зависящая от них(но также может зависеть только от первоначальной библиотеки) библиотека для рисования графиков. А затем весь этот набор идет в само приложение.
Почему так?
Мне важно было сделать так, чтобы пользователь, которому нужна только часть функционала, получал именное её. Да и эти библиотеки в дальнейшем можно будет использовать отдельно от приложения.
Язык программирования
Следующий важный выбор это на чём же писать?
Для этого проекта я перепробовал множество языков программирования и в своём выборе пользовался несколькими критериями(один из которых просто основан на моей вкусовщине):
-
Возможная скорость программы и вычислений. Всё‑таки это остаётся важным пунктом, так как это та программа, где важно минимизировать траты системных ресурсов.
-
Простота написания и поддержки кода. Крайне важно, чтобы ваша программа не причиняла трудности в тот момент, когда вы хотите что‑то исправить.
-
Парадигма языка и его новизна. Этот критерий по сути крайне нелогичен, но тут уже хотелось просто сделать такой выбор, чтобы было крайне приятно писать то, что является по сути моим хобби.
В ходе долгих размышлений о том, что лучше я решил остановиться на языке программирования Zig, но почему?
Потому что, этот ЯП обладает достаточно большим потенциалом для написания программ, где производительность играет критическую роль, а также, изучая его, я понял, что его синтаксис и философия это то, что идеально подойдёт мне под второй и третий пункт.
Пункт третий: реализация
На данный момент готова лишь первая библиотека, но и про неё есть, что рассказать.
Первым решением, которое я принял, было написании библиотеки так, чтобы её можно было использовать с вещественным числом любой точности вплоть до 128 битного.
Я решил реализовать это следующим образом:
Скрытый текст
// 1. Define a user option (e.g., -Duse_f64=true), defaulting to falseconst use_f64 = b.option(bool, "use_f64", "Use f64 instead of f32 for precision") orelse false;const use_f128 = b.option(bool, "use_f128", "Use f128 instead of f32 for precision") orelse false;const mathfn = b.option(bool, "mathfn", "compile with mathfn module") orelse false;// 2. Create an OptionsStep and add our optionconst options = b.addOptions();options.addOption(bool, "use_f64", use_f64);options.addOption(bool, "use_f128", use_f128);options.addOption(bool, "mathfn", mathfn);......mod.addOptions("cfg", options);
То есть при сборке библиотеки нужно выбрать соответствующий тип.
Затем нужно было написать классическую реализацию парсера и функции для вычисления выражений.
Tokenizer.zig — код токенайзера
pub const TokenType = enum { Number, Identifier, Plus, Minus, Mul, Div, Pow, LParen, RParen, Comma, EOF,};pub const Token = struct { tok_type: TokenType, value: []const u8,};pub const Tokenizer = struct { input: []const u8, pos: usize, current_tok: Token, pub fn init(alloc: std.mem.Allocator, input: []const u8) !Tokenizer { const clean_str = try remove_whitespaces(alloc, input); var t = Tokenizer{ .input = clean_str, .pos = 0, .current_tok = undefined, }; t.advance(); return t; } pub fn advance(self: *Tokenizer) void { if (self.pos >= self.input.len) { self.current_tok = .{ .tok_type = .EOF, .value = "" }; return; } const start = self.pos; const char = self.input[start]; if (ascii.isDigit(char) or char == '.') { make_num(self, start); } else if (ascii.isAlphabetic(char) or char == '_') { make_identifier(self, start); } else { make_symbol(self, char); } } pub fn deinit(self: *Tokenizer, alloc: std.mem.Allocator) !void { return alloc.free(self.input); } fn make_num(self: *Tokenizer, start: usize) void { while (self.pos < self.input.len and (std.ascii.isDigit(self.input[self.pos]) or self.input[self.pos] == '.')) : (self.pos += 1) {} self.current_tok = .{ .tok_type = .Number, .value = self.input[start..self.pos], }; } fn make_identifier(self: *Tokenizer, start: usize) void { while (self.pos < self.input.len and (std.ascii.isAlphanumeric(self.input[self.pos]) or self.input[self.pos] == '_')) : (self.pos += 1) {} self.current_tok = .{ .tok_type = .Identifier, .value = self.input[start..self.pos], }; } fn make_symbol(self: *Tokenizer, c: u8) void { self.pos += 1; switch (c) { '+' => self.current_tok = .{ .tok_type = .Plus, .value = "+" }, '-' => self.current_tok = .{ .tok_type = .Minus, .value = "-" }, '*' => self.current_tok = .{ .tok_type = .Mul, .value = "*" }, '/' => self.current_tok = .{ .tok_type = .Div, .value = "/" }, '^' => self.current_tok = .{ .tok_type = .Pow, .value = "^" }, '(' => self.current_tok = .{ .tok_type = .LParen, .value = "(" }, ')' => self.current_tok = .{ .tok_type = .RParen, .value = ")" }, ',' => self.current_tok = .{ .tok_type = .Comma, .value = "," }, else => self.current_tok = .{ .tok_type = .EOF, .value = "" }, } }};
Parser.zig — код самого парсера
pub const Node = struct { kind: Kind, data: Data, pub const Kind = enum { Number, Identifier, BinaryOp, UnaryOp, FunctionCall }; pub const Data = union(enum) { Number: Float, Identifier: []const u8, BinaryOp: struct { op: Op, left: *Node, right: *Node }, UnaryOp: struct { op: Op, operand: *Node }, FunctionCall: struct { name: []const u8, args: []*Node }, }; fn deinit(self: *Node, alloc: std.mem.Allocator) void { switch (self.*.data) { .Number => {}, .Identifier => {}, .BinaryOp => |str| { str.left.deinit(alloc); alloc.destroy(str.left); str.right.deinit(alloc); alloc.destroy(str.right); }, .UnaryOp => |str| { str.operand.deinit(alloc); alloc.destroy(str.operand); }, .FunctionCall => |str| { for (str.args) |arg| { arg.deinit(alloc); alloc.destroy(arg); } }, } } pub fn cleanup(self: *Node, alloc: std.mem.Allocator) void { self.deinit(alloc); alloc.destroy(self); }};pub const ParseError = error{ InvalidNumber, UnexpectedToken, MissingRParen, DivisionByZero, UnknownVariable, UnknownFunction, InvalidFunctionArgs, OutOfMemory,};pub const Parser = struct { t: *tokenizer.Tokenizer, pub fn init(tz: *tokenizer.Tokenizer) !Parser { return .{ .t = tz }; } pub fn parseExpression(self: *Parser, alloc: std.mem.Allocator) ParseError!*Node { return self.parseAddition(alloc); } fn parseAddition(self: *Parser, alloc: std.mem.Allocator) ParseError!*Node { var left = try self.parseMultiplication(alloc); while (self.t.current_tok.tok_type == .Plus or self.t.current_tok.tok_type == .Minus) { const op_token = self.t.current_tok; self.t.advance(); const right = try self.parseMultiplication(alloc); const op = if (op_token.tok_type == .Plus) Op.Add else Op.Sub; left = self.makeNode(alloc, .BinaryOp, .{ .BinaryOp = .{ .op = op, .left = left, .right = right } }) catch { return error.OutOfMemory; }; } return left; } fn parseMultiplication(self: *Parser, alloc: std.mem.Allocator) ParseError!*Node { var left = try self.parsePower(alloc); while (self.t.current_tok.tok_type == .Mul or self.t.current_tok.tok_type == .Div) { const op_token = self.t.current_tok; self.t.advance(); const right = try self.parsePower(alloc); const op = if (op_token.tok_type == .Mul) Op.Mul else Op.Div; left = self.makeNode(alloc, .BinaryOp, .{ .BinaryOp = .{ .op = op, .left = left, .right = right } }) catch { return error.OutOfMemory; }; } return left; } fn parsePower(self: *Parser, alloc: std.mem.Allocator) ParseError!*Node { const base = try self.parseUnary(alloc); if (self.t.current_tok.tok_type == .Pow) { self.t.advance(); const exp = try self.parsePower(alloc); // Right-associative return self.makeNode(alloc, .BinaryOp, .{ .BinaryOp = .{ .op = Op.Pow, .left = base, .right = exp } }) catch { return error.OutOfMemory; }; } return base; } fn parseUnary(self: *Parser, alloc: std.mem.Allocator) ParseError!*Node { if (self.t.current_tok.tok_type == .Plus or self.t.current_tok.tok_type == .Minus) { const op_token = self.t.current_tok; self.t.advance(); const operand = try self.parseUnary(alloc); if (op_token.tok_type == .Minus) { return self.makeNode(alloc, .UnaryOp, .{ .UnaryOp = .{ .op = Op.Neg, .operand = operand } }) catch { return error.OutOfMemory; }; } return operand; } return self.parsePrimary(alloc); } fn parsePrimary(self: *Parser, alloc: std.mem.Allocator) ParseError!*Node { const tok = self.t.current_tok; switch (tok.tok_type) { .Number => { self.t.advance(); const val = std.fmt.parseFloat(Float, tok.value) catch return error.InvalidNumber; return self.makeNode(alloc, .Number, .{ .Number = val }) catch { return error.OutOfMemory; }; }, .Identifier => { self.t.advance(); if (self.t.current_tok.tok_type == .LParen) { self.t.advance(); return self.parseFunctionCall(alloc, tok.value); } return self.makeNode(alloc, .Identifier, .{ .Identifier = tok.value }) catch { return error.OutOfMemory; }; }, .LParen => { self.t.advance(); const expr = try self.parseExpression(alloc); if (self.t.current_tok.tok_type != .RParen) return error.MissingRParen; self.t.advance(); return expr; }, else => return error.UnexpectedToken, } } fn parseFunctionCall(self: *Parser, alloc: std.mem.Allocator, name: []const u8) ParseError!*Node { var args_list = std.ArrayList(*Node).empty; defer args_list.deinit(alloc); if (self.t.current_tok.tok_type != .RParen) { args_list.append(alloc, try self.parseExpression(alloc)) catch { return error.OutOfMemory; }; while (self.t.current_tok.tok_type == .Comma) { self.t.advance(); args_list.append(alloc, try self.parseExpression(alloc)) catch { return error.OutOfMemory; }; } } if (self.t.current_tok.tok_type != .RParen) return error.MissingRParen; self.t.advance(); const args = alloc.dupe(*Node, args_list.items) catch { return error.OutOfMemory; }; return self.makeNode(alloc, .FunctionCall, .{ .FunctionCall = .{ .name = name, .args = args } }) catch { return error.OutOfMemory; }; } inline fn makeNode(self: *Parser, alloc: std.mem.Allocator, kind: Node.Kind, data: Node.Data) !*Node { _ = self; const node = try alloc.create(Node); node.* = .{ .kind = kind, .data = data }; return node; }};
Eval.zig — код произведения вычислений
const isMathFnEnabled = @import("cfg").mathfn;const Mathfn = if (isMathFnEnabled) @import("mathfn/mathfn.zig") else struct {};const Fun = if (isMathFnEnabled) Mathfn.Fun else struct {};pub fn evaluate(alloc: std.mem.Allocator, node: *parser.Node, variables: *const std.StringHashMap(Float), funcs: *std.StringHashMap(Fun)) parser.ParseError!Float { switch (node.data) { .Number => |v| return v, .Identifier => |name| { return variables.get(name) orelse return error.UnknownVariable; }, .BinaryOp => |b| { const l = try evaluate(alloc, b.left, variables, funcs); const r = try evaluate(alloc, b.right, variables, funcs); switch (b.op) { .Add => return l + r, .Sub => return l - r, .Mul => return l * r, .Div => { if (r == 0) return error.DivisionByZero; return l / r; }, .Pow => return std.math.pow(Float, l, r), else => unreachable, } }, .UnaryOp => |u| { const val = try evaluate(alloc, u.operand, variables, funcs); switch (u.op) { .Neg => return -val, else => return val, } }, .FunctionCall => |fc| { // Temporary buffer for evaluated arguments const args = std.heap.page_allocator.alloc(Float, fc.args.len) catch { return error.UnexpectedToken; }; defer std.heap.page_allocator.free(args); for (fc.args, 0..) |arg, i| { args[i] = try evaluate(alloc, arg, variables, funcs); } return callfn(alloc, fc.name, args, funcs); }, }}fn callfn(alloc: std.mem.Allocator, name: []const u8, args: []Float, funcs: *std.StringHashMap(Fun)) parser.ParseError!Float { if (is_builtin(name)) { return callBuiltin(name, args); } if (!isMathFnEnabled) { std.debug.print("yes", .{}); return error.UnknownFunction; } if (!funcs.contains(name)) { std.debug.print("yes", .{}); return error.UnknownFunction; } const fun = funcs.getPtr(name); if (fun) |callable| { var map_args = try build_args(alloc, args, callable.*); defer map_args.deinit(); return callable.call(alloc, &map_args, funcs); } return error.UnknownFunction;}fn build_args(alloc: std.mem.Allocator, args: []Float, fun: Fun) !std.StringHashMap(Float) { var i: usize = 0; var map = std.StringHashMap(Float).init(alloc); for (fun.args_names.items) |name| { try map.put(name, args[i]); i += 1; } return map;}fn is_builtin(name: []const u8) bool { if (std.mem.eql(u8, name, "sin")) { return true; } else if (std.mem.eql(u8, name, "cos")) { return true; } else if (std.mem.eql(u8, name, "sqrt")) { return true; } else if (std.mem.eql(u8, name, "abs")) { return true; } else if (std.mem.eql(u8, name, "log")) { return true; } return false;}fn callBuiltin(name: []const u8, args: []const Float) parser.ParseError!Float { if (std.mem.eql(u8, name, "sin") and args.len == 1) return @sin(args[0]); if (std.mem.eql(u8, name, "cos") and args.len == 1) return @cos(args[0]); if (std.mem.eql(u8, name, "sqrt") and args.len == 1) return @sqrt(args[0]); if (std.mem.eql(u8, name, "abs") and args.len == 1) return @abs(args[0]); if (std.mem.eql(u8, name, "log") and args.len == 1) return @log(args[0]); return error.UnknownFunction;}
Если про первые два файла можно сказать, что это совершенно типовая реализация подобной задачи, то про последний стоит рассказать поподробнее(я не зря вставил некоторые импорты в код). Здесь я решил продолжить идею максимальной модульности и вынести код функций пользователя в отдельный подмодуль Mathfn, который можно отдельно включить подобно тому, как настраивается тип вещественных чисел.
Его реализации представляет собой структуру которая сначала пробегается по строке, парсит математические выражения и аргументы функции.
Подробно код можно посмотреть здесь: Mukas / mathexpr · GitLab
Заключение
Подводя итоги, хочу ещё раз сказать, как сильно влияет профессиональное образование на наши хобби. Я бы сказал, что именно оно и влияет на то, кем мы становимся и что делаем. Моя история служит тому примером.
Про свой проект хочу сказать, что хоть он и не идеален и даже еще и не близок к тому, чтобы быть законченным, но это ценный опыт, который я получаю. Хочу сказать: никогда не бойтесь реализовывать свои идеи, в этом деле всегда важно начать. Даже просто с ручки и бумаги
ссылка на оригинал статьи https://habr.com/ru/articles/1058464/