π Draupnir Project Templates
Draupnir is a magical ring that symbolizes prosperity and abundance. Every ninth night, Draupnir would produce eight new rings of the same size and weight, adding to Odinβs wealth and power. This magical ring adds to the allure of Odinβs arsenal and the mythology that surrounds it.
In the same spirit, this project adds to Odinβs wealth and power by scaffolding Odin projects.
Odinβs Arsenal
This project is part of the Odinβs Arsenal.
How to Download
Download draupnir templates with a quick curl command.
Ensure to download the tar.gz version to preserve file permissions.
$ curl -L https://github.com/odin-arsenal/draupnir/archive/refs/heads/main.tar.gz -o templates.tar.gz
Untar the file and rename the template directory of your liking. You can remove the other files and directories.
$ tar βxvzf templates.tar.gz
$ mv draupnir-main/minimal-template my-project
$ rm -rf draupnir-main
$ rm -rf templates.tar.gz
$ cd my-project
Draupnir Command
The steps above were automated in a bash script.
You can download it and put it inside /usr/local/bin
$ curl -L https://github.com/odin-arsenal/draupnir/releases/download/v1.0.0/draupnir.tar.gz -o draupnir.tar.gz
$ tar xvzf draupnir.tar.gz
$ cp draupnir /usr/local/bin
Now you can create templates with draupnir command.
$ draupnir <template> <new-project-directory>
Examples
$ draupnir full my-new-project
$ draupnir minimal my-new-project
$ draupnir blank my-new-project
Modified BSD License
Copyright Β©, Ninjas.cl
All rights reserved.
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
- Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
- Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
- Neither the name of the
Ninjas.clnor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS βAS ISβ AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL Ninjas.cl BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
Credits
Made with ♥ by Ninjas.cl .
Blank Template
This project template only contains: lib/, project.odin, .gitignore and README.md.
Itβs the most lean possible template compatible with Gungnir. Besides the lib and project.odin directories
is up to you to set the contents and structure of the project.
When to use?
Use this template if you want the bare minimun required to be a Draupnir template.
- The most barebones template. Only
project.odinandlib/directory. - See the contents of a blank template.
Instalation
$ draupnir blank my-project
Structure
2 directories, 4 files
.
βββ .gitignore
βββ README.md
βββ lib
βΒ Β βββ .gitkeep
βββ project.odin
Minimal Template
This project template have only the minimum directories and files needed for Gungnir (lib and project.odin), so it can generate documentation. Also contains common files such as: .gitignore, .editorconfig and a sample README.md and test directory.
When to Use?
Use this template if you want a lean starting point with just a lib and project.odin directories.
Ideal for experiments, simple applications or want to tailor the structure at will. It includes the run script that enables the -collection:project=. param, and can build debug and release versions of project.odin.
-
Simple command lines and scripts.
-
Simple libraries.
-
Not many contributors.
Instalation
$ draupnir minimal my-project
Structure
3 directories, 9 files
.
βββ .editorconfig
βββ .gitattributes
βββ .gitignore
βββ README.md
βββ lib
βΒ Β βββ application.odin
βββ project.odin
βββ run
βββ test
βββ lib_test.odin
βββ test_helper.odin
Structure and Files
In the full project template you can find these files and directories. In minimal template just a few of them are present.
-
lib: A directory that holds your application source code. This directory can be broken into subdirectories (packages). -
test: Directory with all of your application tests. It often mirrors the same structure found inlib. -
run: A bash script to run helper commands. -
_build: A directory created by the run command line tool that ships as part of Draupnir that holds all compilation artifacts. This directory must not be checked into version control and it can be removed at any time. Removing it will force the project to rebuild your application from scratch. -
bin: A directory created by the run command line tool that ships as part of Draupnir that holds all compilation artifacts for Release environment. This directory must not be checked into version control and it can be removed at any time. Removing it will force the project to rebuild your application from scratch. -
test/test_helper.odin: A file to set variables and other global settings for the tests. -
test/lib_test.odin: An example test file.
Run Script
The full and minimal templates provides a ./run script to ease commands:
./run build.debug: Builds theproject.odinfile in debug mode../run build.release: Builds theproject.odinfile in production mode and store it inbin../run project: Runs theprojectbinary. You can pass arguments../run version: Builds thedebugbinary and runs the-vcommand../run format: Runs format and lint in the codebase../run docs: Generates documentation usingGungnir../run deps.get: Downloads git submodules insidedeps../run test: Run tests.
Project Collection
The minimal template provides a collection is passed down to odin called project
that points to the root directory.
Before
import "../../../config"
After
import "project:config"
Env Define
Minimal templates can use a define named env is pass down in build scripts
that can be used for special setting in different build configurations.
#config(env, "dev")
Full Template
This project template have all the bells and whistles of a complete project: Readmes, Changelogs, dependency directory, docs, private directores, and other directories and files. Normally used in complex applications and command line apps.
When to Use?
Use this template if you want a complete application scaffold from the start. It provides a good and strong structure for big applications to grow. It includes the run script that enables the -collection:project=. param, and can build debug and release versions of project.odin.
-
Big applications.
-
Graphical applications (Games, Desktop, Complex Command lines).
-
Applications that could become bigger over time.
-
Applications that could have many contributors (Open source).
Instalation
$ draupnir full my-project
Structure
23 directories, 43 files
.
βββ .editorconfig
βββ .gitattributes
βββ .gitignore
βββ .hooks
βΒ Β βββ Makefile
βΒ Β βββ pre-commit
βΒ Β βββ pre-commit.d
βΒ Β βββ 01-format-and-lint
βββ .tool-versions
βββ AUTHORS.md
βββ CHANGELOG.md
βββ CONTRIBUTING.md
βββ LICENSE.md
βββ README.md
βββ SUMMARY.md
βββ config
βΒ Β βββ config.odin
βΒ Β βββ runtime.odin
βββ deps
βΒ Β βββ .gitkeep
βββ docs
βΒ Β βββ .gitkeep
βββ lib
βΒ Β βββ application.odin
βΒ Β βββ domain
βΒ Β βΒ Β βββ domain.odin
βΒ Β βββ lib.docg
βΒ Β βΒ Β βββ Example.md
βΒ Β βΒ Β βββ SUMMARY.md
βΒ Β βΒ Β βββ resources
βΒ Β βΒ Β βββ .gitkeep
βΒ Β βββ tasks
βΒ Β βββ options
βΒ Β βΒ Β βββ options.odin
βΒ Β βββ tasks.odin
βΒ Β βββ version
βΒ Β βββ task.odin
βββ priv
βΒ Β βββ documentation
βΒ Β βΒ Β βββ mdbook
βΒ Β βΒ Β βββ .gitignore
βΒ Β βΒ Β βββ book.toml
βΒ Β βΒ Β βββ resources
βΒ Β βΒ Β βΒ Β βββ .gitkeep
βΒ Β βΒ Β βββ run
βΒ Β βΒ Β βββ theme
βΒ Β βΒ Β βββ book.js
βΒ Β βΒ Β βββ highlight.js
βΒ Β βΒ Β βββ styles
βΒ Β βΒ Β βββ github-dark.min.css
βΒ Β βββ logs
βΒ Β βΒ Β βββ .gitkeep
βΒ Β βββ scripts
βΒ Β βββ build-debug
βΒ Β βββ build-release
βΒ Β βββ format
βΒ Β βββ test
βββ project.ini
βββ project.odin
βββ resources
βΒ Β βββ .gitkeep
βββ run
βββ test
βββ lib_test.odin
βββ test_helper.odin
Components
The full project contains an additional component called tasks which help creating command line apps by parsing the arguments and providing a common structure for running tasks. A common logger configuration and a project.ini parser.
Tasks
A task can be defined by creating a new package inside lib/tasks, and which defines the run procedure.
The run procedure will receive a list of all command line arguments passed, according to the userβs terminal.
You can use these tasks in project.odin as a way to access internal lib data or procedures.
Example Task
package Tasks_Version
import "core:fmt"
import "project:config"
import "../options"
run :: proc(args: options.Options) {
fmt.printfln("%s\n%s\n%s", config.COPYRIGHT, config.AUTHORS, config.WEBSITE)
}
Logger
Additionally the full template comes with a predefined logger inside context.
It will save inside priv/logs/console.log by default.
// Start Logger
log_handle := setup_logger()
context.logger = log.create_multi_logger(
log.create_console_logger(),
log.create_file_logger(log_handle),
)
Project configuration (project.ini)
The file project.ini is read on main procedure (project.odin) and stored it in the shared context. Itβs values are available
to all procedures that share the same context. This file is meant to be used by other command line apps
to gather meta information about the project. Can also be used as settings that an user can modify to give runtime user configurable information. Parsing ini files is supported by the core Odin libraries.
settings := read_project_settings()
context.user_ptr = &settings
mdBook configuration
The project comes with an mdBook ready directory including Odin highlight configuration.
You can configure it inside priv/documentation/mdbook directory.
Structure and Files
In the full project template you can find these files and directories. In minimal template just a few of them are present.
-
lib: A directory that holds your application source code. This directory can be broken into subdirectories (packages). -
lib/domain: This is the functional core of the application. Also called the βbusiness logicβ or domain of the application. Normally is a group of procedures and definitions representing the data layer, organized into packages. -
test: Directory with all of your application tests. It often mirrors the same structure found inlib. -
priv: A directory that keeps all resources that are necessary in production but are not directly part of your source code. You typically keep database scripts, translation files, images, and more in here. Generated assets, created from Gungnir are placed inpriv/documentationby default. -
deps: Directory for all dependencies (external libraries and vendored code) of your application. It normally contains Git Submodules. -
docs: Documentation generated to be available in Github Pages or similar. -
lib.docg:Gungnirdocumentation catalogue. -
SUMMARY.md: A file used inGungnirto generate a menu. -
README.md: The main README of the project. -
AUTHORS.md: Who is the team behind the project. -
CHANGELOG.md: Which changes the project had overtime. -
LICENSE.md: What license the project adheres to?. -
CONTRIBUTING.md: A quick introducction to the project, tailored for developers. -
project.ini: A simple*.iniconfiguration file to store metadata about the project. Like version number and project name. May be used by tools such as Gungnir to obtain information about the project. -
.tool-versions:asdf-vmversion declaration of tools for this project. -
run: A bash script to run helper commands. -
_build: A directory created by the run command line tool that ships as part of Draupnir that holds all compilation artifacts. This directory must not be checked into version control and it can be removed at any time. Removing it will force the project to rebuild your application from scratch. -
bin: A directory created by the run command line tool that ships as part of Draupnir that holds all compilation artifacts for Release environment. This directory must not be checked into version control and it can be removed at any time. Removing it will force the project to rebuild your application from scratch. -
.hooks: store git hooks for linting and formatting odin projects. -
config/: A directory that holds your project configuration. Theconfig/config.odinfile is the entry point for your configuration. -
config/config.odin: Stores configuration settings for different environments: dev, prod, runtime, test. -
config/runtime.odin: Used for setting or getting Environment variables and other runtime only settings. -
resources: Stores different static assets like images needed for the application. -
priv/documentation: AGungnirdirectory to store the amalgamated documentation beforemdbookcompilation. -
priv/scripts: Project helpful shell scripts for diverse tasks. -
priv/logs: Project log registry. -
test/test_helper.odin: A file to set variables and other global settings for the tests. -
test/lib_test.odin: An example test file.
Run Script
The full and minimal templates provides a ./run script to ease commands:
./run build.debug: Builds theproject.odinfile in debug mode../run build.release: Builds theproject.odinfile in production mode and store it inbin../run project: Runs theprojectbinary. You can pass arguments../run version: Builds thedebugbinary and runs the-vcommand../run hooks: Install hooks to.gitdirectory../run format: Runs format and lint in the codebase../run docs: Generates documentation usingGungnir../run deps.get: Downloads git submodules insidedeps../run test: Run tests.
Project Collection
The full template provides a collection is passed down to odin called project
that points to the root directory.
Before
import "../../../config"
After
import "project:config"
Env Define
The full template can use a define named env is pass down in build scripts
that can be used for special setting in different build configurations
(in full template inside config/config.odin).
when ENVIRONMENT == "dev" {
}
when ENVIRONMENT == "prod" {
}
when ENVIRONMENT == "test" {
}
Why use Draupnir Templates?
These templates have in common the project.odin file and the lib directory. These are the core structures that a Draupnir template requires. Following these conventions, many tools can have a common standard and help the community understand quicker and better Odin codebases. For example the Gungnir tool can assume the codebase contains at minimum a project.odin file and a lib directory and start searching for files to document inside the lib directory. This convention simplifies many decisions and make tools more thoughtful and friendly.
Conventions
Draupnir follows some basic structure for applications in Odin.
lib/ directory
lib directory holds your application source code. This directory is broken into subdirectories (packages). For example, the lib/domain directory will be responsible to host all of your business logic and business domain.
It was chosen lib instead of src because the main application project.odin requires this directory as a βlibraryβ.
project.odin file
This is a file that identifies our directory as an Odin project. It also contains the main :: proc() declaration. In full template is used to define common data structures such as command line arguments, logger and project.ini configurations.
Execute project.odin
$ odin run .
Ada_Case package names
Package naming doesnβt need to be a mirror of your OS directories. Itβs good when it can, but we donβt need to ruin their naming for the sake of the OS system.
For example
βββ controllers
βββ account_controller.odin
βββ bread_controller.odin
βββ user_controller.odin
Te package must be named like Project_User_Controller, and not project_user_controller. The convention for naming packages is using Ada_Case.
Is recommended to use a project prefix for package names. For example if my project is named Drawings the package names could be Drawings_Circle and Drawings_Triangle, even though the packages could be in deep hierarchy.
package Drawings
main :: proc() {}
Package directories
Package directories can be lowercase or PascalCase. The only exception is the project.odin level directories that always must be lowercase. PascalCase is recommended when the package could conflict with a built in package. For example string, a package with the name String would not conflict. Is left to the developer the convention used, but they must be consistent in the whole project.
Where these conventions comes from?
This convention takes inspiration and ideas from battle tested projects found in other languages and tools such as:
- Elixir Mix. Base idea for the βminimal templateβ.
- Phoenix Framework. Base idea for the βfull templateβ.
- Gleam. Base idea for the
project.ini.
State of the Art
Odin ecosystem seeems to not have an standard way of file and project organization. Letβs analyze a bunch of Odin projects randomly selected:
Podin
- https://codeberg.org/mgavioli/podin
Odin Aseprite
- https://github.com/blob1807/odin-aseprite
Odin Mimalloc
- https://github.com/jakubtomsu/odin-mimalloc
Odin HTTP
- https://github.com/laytan/odin-http
A standarization will help the creation of tools such as IDEs, documentation and other developer experience improvements, increase the speed of understanding the codebase by new people and provide a solid structure for making projects with Odin. Using conventions such as the provided by Draupnir will speed up the development efforts of Odinβs ecosystem.
Structuring Odin Projects
Odin is a programming language where the code is mainly organized as procedures and packages (a directory of files). However, Odin documentation doesnβt give a lot of advice on how to organize a project, as expected from a general-purpose language. A good project organization enable our systems to be versatile enough to cover all existing and future features, while remaining consistent. It should also allow scaling small subfeatures into larger ones. A good project structure is that it guides us to design our programs in a way to most benefit from the architecture that they run on.
The rule of thumb when structuring applications, is to allow the top-level directories to directly communicate with layer below, but any communication crossing more than one line is frown upon. As an example, project.odin can retrieve packages in lib/tasks. Reaching out from lib/tasks to project.odin is - however - not good.
Try to keep the directory structure as flat as it is possible and manageable. You donβt really need to type more than necessary when opening/saving files - as long as there is sane number of files in a directory. There is no need to multiply subdirectories for the sake of multiplying subdirectories.
The whole idea is to have packages that work as an entry point for your appβs business. Then, all sub-entities for that aspect live there, on inner files and directories. Itβs like your lib has sub-libraries. Then your sub-library has a main interface that lives on an entry point package. The main reasoning is thinking about the deletion and moving. If you want to move a package to another directory, all its files should move together. See the entry point purpose working like an index file in a web server directory, and usually, the index files live in the directory as children, not as siblings.
lib/social_network
βββ authentication
βββ authentication.odin
βββ google.odin
βββ facebook.odin
βββ password.odin
Avoid namespacing, use Sufixes
Some kinds of files fit in one or more categories and do not belong exclusively to a common package. For example, errors, your app might have several packages that represent errors. Letβs say you have multiple errors in your app: argument, arithmetic, not found, etc. They are all errors, but they should not belong to a single βError packageβ. Instead of creating a namespace, you can use a suffix. Your naming would look like this:
- package.ArgumentError
- package.ArithmeticError
- package.NotFoundError
- package.TemporarilyUnavailableError
So they arenβt tied anymore to the unhelpful namespacing and concept βErrorβ, now youβre free to move them to sub or parent applications as the time needs. As long as your project grows, these files that arenβt together conceptually might pollute your OS directories and make things hard to navigate. So itβs okay to organise your OS files and directories in a way that works better.
Building a Functional Core in Odin
The book Designing Elixir Systems with OTP shows the concept of βDo Fun Things with Big, Loud Worker-Beesβ (a mnemonic for understanding the layers). A concept tailored to design libraries with intelligent layers that shape the right data structures, flow from one procedures into the next, and present the right APIs. It shows you how to go beyond simple programming to designing, and that means building the right layers. Embrace data structures that work best and use them to build procedures that perform and compose well, layer by layer, across packages. Test your code at the right place using the right techniques. Layer your code into pieces that are easy to understand. The objective when using these techniques is making the codebases a lot simpler.
Functional Core (Do Fun Things)
This is the βcoreβ, βkernelβ or βdomainβ. Is the main business logic unit that is composed by Data, Functions and Procedures and Tests.
Data
The idea is to rely on inmutable data structures as much as posible, so applications get much easier to maintain and algorithms get much cleaner, achieving harmony between data structures and procedures.
Odin gives developers several data structures. Structs, Maps, Enums, Strings, but also procedures that can be data too. Sometimes using procedures as data params can offer tremendous perfomance wins.
Example
We could store our Square with the data.
Square {
{5, 0}, {15, 0}
{15, 0}, {15, 10}
{15, 10}, {5, 10}
{5, 10}, {5, 0}
}
This way works fine, but we could also convert it to a procedure that calculates the values. That way we can avoid passing and coping heavy data in the params.
package Data
import "core:fmt"
Square :: struct {
x1, y1, x2, y2, x3, y3, x4, y4 : [2]int
}
square :: proc (x: int, y: int, size: int) -> Square {
return Square{
{x, y}, {x + size, y},
{x + size, y}, {x + size, y + size},
{x + size, y + size}, {x, y + size},
{x, y + size}, {x, y}
}
}
main :: proc() {
// We can save the procedure in a variable
// Or pass the procedure as a param in another procedure
// This is passing the pointer
my_procedure_pointer := square
// Square{x1 = [5, 0], y1 = [15, 0],
// x2 = [15, 0], y2 = [15, 10],
// x3 = [15, 10], y3 = [5, 10],
// x4 = [5, 10], y4 = [5, 0]}
fmt.printf("%v", my_procedure_pointer(5, 0, 10))
}
We start with a procedure called square. It takes a point (x, y) and a size, and transforms that data to the same square format we saw earlier.
Procedures
Procedures needs certainty and sanitized data. If you were building an application it would be more of a library. The main concept here is CRC, which means: Constructors, Reducers and Converters.
-
Constructors: Are procedures that creates and sets data structures that will be pased down toReducersandConverters. This data structure is known as theaccumulatorortoken. -
Reducers: These procedures take theaccumulatorand βreduce itβ, applying a pipeline of different operations and procedures until it reaches a state ready for theConverter. -
Converter: Takes the accumulator and βconvert itβ to a final format, that is ready for displaying to the user or pass it to another pipeline of CRC. Example would be a procedure that takes ajsondata structure and convert it tostring, this string will be passed down to the pipeline for saving the contents to a file in disk.
Thanks to this form of organization, we can see our code in a coherent and unified way. By having a common data structure with several functions, we can perform operations and express ourselves with greater readability. Our systems will be easier to understand and maintain in the future.
Consistency is an important quality factor in our systems and using CRC is a great tool to achieve that.
Example
Odin uses packages, procedures and data types, which allows a way to organize our code in what is called CRCs (Constructors, Reducers, and Converters). So we will have procedures to (create) an accumulator, procedures that will perform operations (reducers) with this accumulator and finally procedures that will transform the accumulator into a final format (converters). This is something that has existed for a long time in different programming languages ββsuch as Haskell or Lisp. The most important thing you can do in a language is to unite ideas using composition. The idea is to create a pipeline that receives an accumulator as its first parameter and performs a series of reduce operations until reaching the final converter.
package CRC
import "core:fmt"
Person :: struct {
name : string,
age : int
}
// Creator
new_person :: proc(name: string, age: int) -> Person {
person : Person
person.name = name
person.age = age
return person
}
// Reducer
// We use a new structure to avoid mutating the original parameter
rejuvenate_person :: proc(person: Person, years: int) -> Person {
return new_person(person.name, person.age - years)
}
// Converter, we only show the value we want to.
person_to_string :: proc(person: Person) -> (string, string, int) {
return "The person %s, now have %d years", person.name, person.age
}
// Pipeline
main :: proc() {
person := new_person("Camilo", 35)
person = rejuvenate_person(person, 10)
out, name, age := person_to_string(person)
fmt.printfln(out, name, age) // The person Camilo, now have 25 years
}
Example 2
Letβs illustrate this a bit using a treasure map. In this map we are going to give a series of directions north, south, east and west. You can see that we have a creation function that returns a Treasure struct {x, y}, which will be our accumulator data structure. Then we have a series of reducers that modify the struct and return a new struct with the appropriate values. Finally we have our converter that returns a String with a final message.
package CRC
import "core:fmt"
Treasure :: struct {
x: int,
y: int
}
// Constructors
new :: proc(x: int, y: int) -> Treasure {
treasure : Treasure
treasure.x = x
treasure.y = y
return treasure
}
empty :: proc() -> Treasure {
return new(0, 0)
}
// Reducers
north :: proc(treasure : Treasure) -> Treasure {
return new(treasure.x, treasure.y - 1)
}
south :: proc(treasure : Treasure) -> Treasure {
return new(treasure.x, treasure.y + 1)
}
east :: proc(treasure : Treasure) -> Treasure {
return new(treasure.x - 1, treasure.y)
}
west :: proc(treasure : Treasure) -> Treasure {
return new(treasure.x + 1, treasure.y)
}
// Converter
show :: proc (treasure : Treasure) -> (string, int, int) {
return "The treasure is on the coordinates (%d, %d)", treasure.x, treasure.y
}
// Pipeline
main :: proc() {
treasure := empty() // (0, 0)
treasure = north(treasure) // (0, -1)
treasure = north(treasure) // (0, -2)
treasure = north(treasure) // (0, -3)
treasure = north(treasure) // (0, -4)
treasure = west(treasure) // (1, -4)
treasure = west(treasure) // (2, -4)
treasure = west(treasure) // (3, -4)
treasure = south(treasure) // (3, -3)
treasure = east(treasure) // (2, -3)
treasure = east(treasure) // (1, -3)
treasure = east(treasure) // (0, -3)
out, x, y := show(treasure)
fmt.printfln(out, x, y) // The treasure is on the coordinates (0, -3)
}
We can compare these techniques with the Object Oriented pattern of Command - Query Segregation. Bertrand Meyer in his book Object Oriented Software Construction.
The fundamental idea is that we should divide an objectβs methods into two sharply separated categories:
Queries: Return a result and do not change the observable state of the system (are free of side effects).Commands: Change the state of a system but do not return a value.
Because the term βcommandβ is widely used in other contexts, is preferred to refer to them as βmodifiersβ, βmutatorsβ, or in this case reducers. The really valuable idea in this principle is that itβs extremely handy if you can clearly separate methods that change state from those that donβt. This is because you can use queries in many situations with much more confidence, introducing them anywhere, changing their order. You have to be more careful with modifiers.
Example 3
User @thetarnav points that this can be achieved like the following code too.
Treasure :: [2]int
NORTH :: Treasure{ 0, -1}
SOUTH :: Treasure{ 0, +1}
EAST :: Treasure{-1, 0}
WEST :: Treasure{+1, 0}
treasure := \
NORTH + // (0, -1)
NORTH + // (0, -2)
NORTH + // (0, -3)
NORTH + // (0, -4)
WEST + // (1, -4)
WEST + // (2, -4)
WEST + // (3, -4)
SOUTH + // (3, -3)
EAST + // (2, -3)
EAST + // (1, -3)
EAST // (0, -3)
Nevertheless is an interesting way of approaching system organization. This is using Array programming techniques where basic operations (+, -, *, etc.) on two fixed-size arrays of the same length are done element-wise. One of the goodies of Odin.
Tests
With proper structure of your projects testing will be much simpler. It will be possible to represent testing concepts in any way you need.
Final Thoughts
The internal building blocks of any project can be dividen in datatypes, procedures and tests. The datatypes guide the structure of components and interactions between procedures. Those procedures are divided along the obvious lines of purpose (CRC). And by separating the project in layers we can use tests to verify what weβve done, using conventional techniques to test the core and other layers.
References
- https://goulven-clech.dev/2024/elixir-folder-structure
- https://www.amberbit.com/blog/2017/11/21/structuring-elixir-projects/
- https://redrapids.medium.com/learning-elixir-its-all-reduce-204d05f52ee7
- https://ulisses.dev/elixir/2022/03/04/elixir-modules-files-directories-and-naming-conventions.html
- https://en.wikipedia.org/wiki/Domain-driven_design
- https://kevinhoffman.medium.com/building-a-functional-core-in-elixir-6201ddcb4300
- https://elixirmerge.com/p/structured-program-design-in-elixir-with-bruce-tate
- https://pragprog.com/titles/jgotp/designing-elixir-systems-with-otp/
- https://martinfowler.com/bliki/CommandQuerySeparation.html