πŸ’ 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:

  1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
  2. 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.
  3. Neither the name of the Ninjas.cl nor 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.

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.

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 in lib.

  • 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 the project.odin file in debug mode.
  • ./run build.release: Builds the project.odin file in production mode and store it in bin.
  • ./run project: Runs the project binary. You can pass arguments.
  • ./run version: Builds the debug binary and runs the -v command.
  • ./run format: Runs format and lint in the codebase.
  • ./run docs: Generates documentation using Gungnir.
  • ./run deps.get: Downloads git submodules inside deps.
  • ./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).

  • See the contents of the Full Template.

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 in lib.

  • 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 in priv/documentation by 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: Gungnir documentation catalogue.

  • SUMMARY.md: A file used in Gungnir to 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 *.ini configuration 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-vm version 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. The config/config.odin file 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: A Gungnir directory to store the amalgamated documentation before mdbook compilation.

  • 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 the project.odin file in debug mode.
  • ./run build.release: Builds the project.odin file in production mode and store it in bin.
  • ./run project: Runs the project binary. You can pass arguments.
  • ./run version: Builds the debug binary and runs the -v command.
  • ./run hooks: Install hooks to .git directory.
  • ./run format: Runs format and lint in the codebase.
  • ./run docs: Generates documentation using Gungnir.
  • ./run deps.get: Downloads git submodules inside deps.
  • ./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

Podin

  • https://codeberg.org/mgavioli/podin

Odin Aseprite

Odin Aseprite

  • https://github.com/blob1807/odin-aseprite

Odin Mimalloc

Odin Mimalloc

  • https://github.com/jakubtomsu/odin-mimalloc

Odin HTTP

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 to Reducers and Converters. This data structure is known as the accumulator or token.

  • Reducers: These procedures take the accumulator and β€œreduce it”, applying a pipeline of different operations and procedures until it reaches a state ready for the Converter.

  • 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 a json data structure and convert it to string, 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