# Tooling and Ecosystem Standards for Nim
This document outlines the recommended tooling and ecosystem standards for Nim development. Following these guidelines ensures maintainable, performant, and secure code, leveraging the latest features and libraries available in the Nim ecosystem. These standards are intended to guide both developers and AI coding assistants.
## 1. Dependency Management
Choosing the right dependency manager and using it effectively is crucial for project reproducibility and maintainability.
**Standard:** Use "nimble" for dependency management.
**Why:** "nimble" is the official package manager for Nim and provides a simple and consistent way to manage project dependencies.
**Do This:**
* Declare dependencies in the "*.nimble" file.
* Specify version constraints using semantic versioning.
* Use "nimble install" to install dependencies.
* Commit the "nimble.lock" file to version control.
**Don't Do This:**
* Manually download and include library code.
* Use wildcard version constraints (e.g., "*").
* Forget to commit the "nimble.lock" file.
**Explanation:**
Using "nimble" and committing "nimble.lock" guarantees that everyone working on the project uses the same versions of dependencies. This prevents "works on my machine" issues caused by version inconsistencies. Semantic versioning helps to avoid breaking changes introduced by new library releases.
**Code Example:**
"""nim
# my_project.nimble
version = "0.1.0"
author = "John Doe"
description = "My awesome Nim project."
license = "MIT"
requires "chronos >= 1.0.0, < 2.0.0" # Chronos version between 1.0.0 and 2.0.0 (exclusive)
requires "docopt == 0.7.1" # Exact docopt version
"""
**Anti-Pattern:**
"""nim
# BAD: No nimble file
import os # what version? where did it come from? prone to errors
"""
## 2. Code Formatting and Linting
Consistent code formatting and linting are essential for readability and team collaboration.
**Standard:** Use "nimpretty" and "nimsuggest" for code formatting and linting. Configure them appropriately and integrate them into your development workflow.
**Why:** "nimpretty" automatically formats code according to a consistent style, while "nimsuggest" detects potential issues and suggests improvements.
**Do This:**
* Install "nimpretty" and "nimsuggest" via "nimble install nimpretty nimsuggest".
* Integrate "nimpretty" into your editor or IDE (e.g., using a plugin or pre-commit hook).
* Configure "nimsuggest" in your editor to display warnings and errors.
* Run "nimpretty" and "nimsuggest" as part of your CI pipeline.
* Adopt a consistent style (e.g., using a ".nimpretty.cfg" file).
**Don't Do This:**
* Ignore formatting and linting warnings.
* Rely solely on manual code formatting.
* Use inconsistent formatting across the codebase.
**Explanation:**
Automated formatting and linting tools enforce a common style, reduce cognitive load, and help catch potential bugs early. Consistent style guides streamline code reviews.
**Code Example:**
First, configure ".nimpretty.cfg":
"""cfg
# .nimpretty.cfg
indentSpaces = 2 # Use 2 spaces for indentation
maxLineLength = 120 # Maximum line length
"""
Then, format all Nim files in your project:
"""bash
nimpretty --config:.nimpretty.cfg --write ./*.nim
"""
**Anti-Pattern:**
"""nim
# BAD: Inconsistent indentation and line breaks
proc calculateArea(width: int,
height: int) : int =
return width * height
"""
## 3. Testing
Comprehensive testing is vital for ensuring code correctness and preventing regressions.
**Standard:** Use the built-in "unittest" module for unit testing. Consider using "spec" for behavior-driven development.
**Why:** The "unittest" module provides a simple and powerful way to write and run tests. "spec" offers an alternative expressive style for defining and verifying code behavior.
**Do This:**
* Write unit tests for all critical code paths.
* Use descriptive test names.
* Use assertions to verify expected behavior.
* Organize tests into logical groups using test suites.
* Run tests frequently, especially before committing changes.
* Use mocking or stubbing to isolate units of code.
**Don't Do This:**
* Skip writing unit tests.
* Write tests that are too broad or too narrow.
* Make assertions based on coincidental behavior.
* Ignore failing tests.
**Explanation:**
Thorough unit testing helps identify bugs early in the development process, reduces the risk of regressions, and improves code maintainability.
**Code Example (unittest):**
"""nim
import unittest
suite "Math functions":
test "Addition":
check 1 + 1 == 2
test "Subtraction":
check 5 - 3 == 2
test "Multiplication":
check 2 * 3 == 6
test "Division":
check 10 div 2 == 5
"""
**Code Example (spec):**
"""nim
import spec
describe "Math functions":
it "should add numbers correctly":
1 + 1 should == 2
it "should subtract numbers correctly":
5 - 3 should == 2
it "should multiply numbers correctly":
2 * 3 should == 6
it "should divide numbers correctly":
10 div 2 should == 5
"""
To run these example tests, save to a ".nim" file and run "nim compile --verbosity:0 --run ".
**Anti-Pattern:**
"""nim
# BAD: No tests
proc calculateArea(width: int, height: int): int =
width * height
"""
## 4. Documentation
Documented code is essential for team collaboration and maintainability.
**Standard:** Use "docgen" and in-code comments to generate API documentation.
**Why:** Documentation makes it easier for others (and your future self) to understand and use your code.
**Do This:**
* Write clear and concise documentation for all public APIs using "##" doc comments.
* Include examples in your documentation.
* Use "docgen" to generate HTML documentation.
* Include a README file explaining how to use the library or application.
* Keep your documentation up to date.
**Don't Do This:**
* Omit documentation for public APIs.
* Write vague or confusing documentation.
* Let your documentation become outdated.
**Explanation:**
Well-documented code reduces the effort required to understand and maintain the codebase. "docgen" makes it easy to generate professional-looking documentation from in-code comments.
**Code Example:**
"""nim
## Calculates the area of a rectangle.
##
## Args:
## width: The width of the rectangle.
## height: The height of the rectangle.
##
## Returns:
## The area of the rectangle.
##
## Example:
## """nim
## let area = calculateArea(5, 10) # area is 50
## """
proc calculateArea(width: int, height: int): int =
width * height
"""
Then generate documentation with:
"""bash
docgen your_module.nim
"""
**Anti-Pattern:**
"""nim
# BAD: No Documentation
proc calculateArea(width: int, height: int): int =
width * height
"""
## 5. Logging
Proper logging helps with debugging and monitoring applications.
**Standard:** Use the "log" module for structured logging.
**Why:** The "log" module provides a flexible and efficient way to log messages with different severity levels.
**Do This:**
* Import the "log" module.
* Use different logging levels (e.g., "debug", "info", "warn", "error", "fatal") appropriately.
* Include contextual information in your log messages.
* Configure logging output to files or other destinations.
**Don't Do This:**
* Use "echo" statements for logging.
* Log sensitive information.
* Log excessively, which can impact performance.
**Explanation:**
Structured logging makes it easier to filter, analyze, and correlate log messages, which is essential for debugging and monitoring applications in production.
**Code Example:**
"""nim
import log
logLevel = lvlInfo # Set default loglevel to Info
try:
let result = 10 div 0
log.info "Result: " & $result
except DivByZeroError:
log.error "Division by zero error"
"""
Configuration can also be done via environment variables:
"""bash
NIM_LOG_LEVEL=debug ./your_program
"""
**Anti-Pattern:**
"""nim
# BAD: Using echo for debugging
proc calculateArea(width: int, height: int): int =
echo "Calculating area with width: ", width, " and height: ", height # Avoid
width * height
"""
## 6. Error Handling
Robust error handling is crucial for preventing crashes and ensuring application stability.
**Standard:** Use "try...except" blocks for handling exceptions. Handle specific exception types whenever possible.
**Why:** Proper error handling prevents unexpected program termination and allows you to gracefully recover from errors.
**Do This:**
* Use "try...except" blocks to catch potential exceptions.
* Handle specific exception types (e.g., "ValueError", "IOError").
* Log error messages with appropriate severity levels.
* Consider using the "Result" type for functions that can fail.
**Don't Do This:**
* Ignore exceptions.
* Use a single "except" block to catch all exceptions.
* Rely on "quit" or "exit" to handle errors.
**Explanation:**
Handling exceptions gracefully prevents crashes and allows you to provide informative error messages to the user. Handling specific exception types allows you to tailor your error handling logic to the specific error that occurred.
**Code Example:**
"""nim
proc readFile(filename: string): Result[string, string] =
try:
let content = readFile(filename)
return ok(content)
except IOError as e:
return err("Error reading file: " & e.msg)
let result = readFile("myfile.txt")
case result:
of ok(content):
echo "File content: ", content
of err(errMsg):
echo "Error: ", errMsg
"""
**Anti-Pattern:**
"""nim
# BAD: Ignoring exceptions
proc calculateArea(width: int, height: int): int =
try:
let area = parseInt(width) * parseInt(height)
except: # Avoid, catch specific exception
echo "Invalid input"
return area
"""
## 7. Concurrency
Nim provides powerful concurrency features. Choosing the right concurrency model is important for performance and correctness.
**Standard:** Use "channels" or "async/await" for concurrent programming. Favor "async/await" for I/O bound operations.
**Why:** "channels" provide a safe and efficient way to communicate between threads. "async/await" simplifies asynchronous programming and improves performance for I/O-bound tasks.
**Do This:**
* Understand the difference between threads and asynchronous tasks.
* Use "channels" for communicating between threads.
* Use "async/await" for handling I/O-bound operations (e.g., network requests, file I/O).
* Avoid shared mutable state when using threads.
* Use locks or atomic operations to protect shared data.
**Don't Do This:**
* Use threads excessively, which can lead to performance problems.
* Share mutable state without proper synchronization.
* Block the main thread with long-running operations.
**Explanation:**
Choosing the right concurrency model is crucial for optimizing performance and preventing race conditions. "async/await" is especially well-suited for I/O-bound tasks, while threads are more appropriate for CPU-bound tasks that can benefit from parallelism.
**Code Example (channels):**
"""nim
import channels, locks
var
channel: Channel[int]
lock: Lock
proc worker(id: int) =
for i in 1..10:
lock.acquire()
try:
channel.send(i * id)
finally:
lock.release()
channel = newChannel[int]()
lock = newLock()
for i in 1..3:
spawn worker(i)
for i in 1..30:
echo channel.recv()
channel.close()
"""
**Code Example (async/await):**
"""nim
import asyncdispatch
import httpclient
asyncProc fetchUrl(url: string): string =
let client = newHttpClient()
let response = await client.get(url)
return response.body
runAsync:
let content = await fetchUrl("https://example.com")
echo content
"""
**Anti-Pattern:**
"""nim
# BAD: Shared mutable state without synchronization causing race conditions
var counter = 0
proc incrementCounter() =
for i in 1..10000:
counter += 1
spawn incrementCounter()
spawn incrementCounter()
sleep(1000)
echo counter # Inconsistent and Wrong Results
"""
## 8. Profiling and Performance Optimization
Identifying and addressing performance bottlenecks is essential for building efficient applications.
**Standard:** Use the built-in profiler and other tools to identify performance bottlenecks.
**Why:** Profiling helps you understand where your application spends its time.
**Do This:**
* Use the "--profiler:on" compiler flag to enable profiling.
* Use "gprof2dot" or similar tools to visualize profiling data.
* Identify hot spots and optimize them.
* Use benchmarking to measure the impact of your optimizations.
* Consider using data structures and algorithms appropriate for the task.
**Don't Do This:**
* Guess at performance bottlenecks.
* Optimize prematurely.
* Ignore profiling data.
**Explanation:**
Profiling provides valuable insights into the performance characteristics of your application, allowing you to focus your optimization efforts where they will have the most impact.
**Code Example:**
Compile with profiling enabled:
"""bash
nim compile --profiler:on your_program.nim
./your_program
gprof your_program | gprof2dot.py -n0 -e0 -f pstats | dot -Tsvg -o profile.svg
"""
Then analyze the "profile.svg" file to visualize your application's performance.
**Anti-Pattern:**
"""nim
# BAD: Premature optimization (assuming string concatenation is always slow)
proc buildString(n: int): string =
var result = ""
for i in 1..n:
result = result & "a" # optimize only IF profiling shows this to be a hotspot
return result
"""
## 9. Tooling Automation
Automate your development workflows to streamline repetitive tasks and improve efficiency.
**Standard:** Use "Makefile" or other automation tools to manage build processes, testing, and documentation generation.
**Why:** Automation reduces the risk of human error and makes it easier to perform common tasks consistently.
**Do This:**
* Create a "Makefile" or other script to automate common tasks.
* Include targets for building, testing, formatting, linting, and documentation generation.
* Use environment variables to configure your automation scripts.
**Don't Do This:**
* Rely solely on manual commands to perform common tasks.
* Duplicate automation logic across multiple scripts.
**Explanation:**
Automation makes it easier to perform common tasks consistently and reduces the risk of human error. A well-defined automation setup can significantly improve development efficiency.
**Code Example (Makefile):**
"""makefile
NAME = my_program
SRC = $(NAME).nim
all: build
build:
nim compile --verbosity:2 $(SRC)
test: build
nim compile --verbosity:0 --run tests/test_$(NAME).nim
format:
nimpretty --write $(SRC)
doc:
docgen $(SRC)
clean:
rm -f $(NAME)
rm -rf doc
"""
Usage:
"""bash
make build
make test
make format
make doc
make clean
"""
## Summary
By adhering to these tooling and ecosystem standards, Nim developers can create more maintainable, performant, and secure applications. Consistent use of the recommended tools and libraries, combined with automated workflows, will contribute to a higher level of code quality and development efficiency.
danielsogl
Created Mar 6, 2025
This guide explains how to effectively use .clinerules
with Cline, the AI-powered coding assistant.
The .clinerules
file is a powerful configuration file that helps Cline understand your project's requirements, coding standards, and constraints. When placed in your project's root directory, it automatically guides Cline's behavior and ensures consistency across your codebase.
Place the .clinerules
file in your project's root directory. Cline automatically detects and follows these rules for all files within the project.
# Project Overview project: name: 'Your Project Name' description: 'Brief project description' stack: - technology: 'Framework/Language' version: 'X.Y.Z' - technology: 'Database' version: 'X.Y.Z'
# Code Standards standards: style: - 'Use consistent indentation (2 spaces)' - 'Follow language-specific naming conventions' documentation: - 'Include JSDoc comments for all functions' - 'Maintain up-to-date README files' testing: - 'Write unit tests for all new features' - 'Maintain minimum 80% code coverage'
# Security Guidelines security: authentication: - 'Implement proper token validation' - 'Use environment variables for secrets' dataProtection: - 'Sanitize all user inputs' - 'Implement proper error handling'
Be Specific
Maintain Organization
Regular Updates
# Common Patterns Example patterns: components: - pattern: 'Use functional components by default' - pattern: 'Implement error boundaries for component trees' stateManagement: - pattern: 'Use React Query for server state' - pattern: 'Implement proper loading states'
Commit the Rules
.clinerules
in version controlTeam Collaboration
Rules Not Being Applied
Conflicting Rules
Performance Considerations
# Basic .clinerules Example project: name: 'Web Application' type: 'Next.js Frontend' standards: - 'Use TypeScript for all new code' - 'Follow React best practices' - 'Implement proper error handling' testing: unit: - 'Jest for unit tests' - 'React Testing Library for components' e2e: - 'Cypress for end-to-end testing' documentation: required: - 'README.md in each major directory' - 'JSDoc comments for public APIs' - 'Changelog updates for all changes'
# Advanced .clinerules Example project: name: 'Enterprise Application' compliance: - 'GDPR requirements' - 'WCAG 2.1 AA accessibility' architecture: patterns: - 'Clean Architecture principles' - 'Domain-Driven Design concepts' security: requirements: - 'OAuth 2.0 authentication' - 'Rate limiting on all APIs' - 'Input validation with Zod'
# Component Design Standards for Nim This document outlines the coding standards for component design in Nim. It aims to provide guidelines for creating reusable, maintainable, and efficient components. These standards leverage Nim's unique features and best practices to ensure high-quality code. The recommendations are based on the latest Nim version and current best practices. ## 1. General Principles ### 1.1. Abstraction * **Do This:** Design components with well-defined interfaces. Hide implementation details behind these interfaces. """nim # Good: Abstract interface type SoundPlayer = interface proc play(filename: string) {.abstract.} type MP3Player* = object of SoundPlayer # Implementation details hidden proc play(filename: string) {.override.} = echo "Playing MP3: " & filename """ * **Don't Do This:** Expose internal data structures or implementation details directly. This leads to tight coupling and makes refactoring difficult. """nim # Bad: Exposing internal data type Database = object connectionString: string # Exposed! """ * **Why:** Abstraction reduces dependencies, making components easier to test, maintain, and reuse. It allows you to change the internal workings of a component without affecting its clients. ### 1.2. Single Responsibility Principle (SRP) * **Do This:** Ensure each component has one specific responsibility. """nim # Good: Separate concerns type EmailSender = object # Handles email sending only proc sendEmail(to: string, subject: string, body: string) type UserValidator = object # Handles user validation only proc validateUser(user: User): bool """ * **Don't Do This:** Create components that handle multiple unrelated tasks. This makes the code harder to understand and modify. """nim # Bad: Multiple responsibilities type UserManager = object # Handles both user management AND email sending proc createUser(user: User) proc sendEmail(to: string, subject: string, body: string) """ * **Why:** SRP improves code clarity and maintainability. When a component only has one responsibility, it's easier to understand, test, and modify. Changes to one responsibility don't affect other parts of the system. ### 1.3. Loose Coupling * **Do This:** Minimize the dependencies between components. Use interfaces, events, and message passing to decouple components. """nim # Good: Loose coupling with interfaces type Logger = interface proc log(message: string) {.abstract.} type ConsoleLogger* = object of Logger proc log(message: string) {.override.} = echo message type FileLogger* = object of Logger filename: string proc log(message: string) {.override.} = writeFile(filename, message & "\n") proc doSomething(logger: Logger) = logger.log("Doing something...") """ * **Don't Do This:** Create tight dependencies between components. This makes it difficult to change or replace one component without affecting others. """nim # Bad: Tight coupling type ComponentA = object b: ComponentB # Directly depends on ComponentB proc doSomething(a: ComponentA) = a.b.someMethod() """ * **Why:** Loose coupling makes components more independent and reusable. Changes in one component are less likely to affect others. This improves the overall flexibility and maintainability of the system. ### 1.4. Cohesion * **Do This:** Ensure the elements within a component are related and work together toward a common purpose. """nim # Good: High cohesion type MathUtils = object # Contains methods related to mathematical operations proc add(a, b: int): int proc subtract(a, b: int): int """ * **Don't Do This:** Create components with unrelated elements or responsibilities. This indicates a lack of cohesion and makes the component harder to understand and maintain. """nim # Bad: Low cohesion type UtilityFunctions = object # Contains unrelated functions proc add(a, b: int): int proc formatDate(date: DateTime): string """ * **Why:** High cohesion makes components easier to understand and maintain. When the elements within a component are related, it's easier to reason about the component's behavior. ### 1.5. Dependency Injection * **Do This:** Inject dependencies into components rather than creating them internally. """nim # Good: Dependency Injection type UserService = object repository: UserRepository # Dependency injected proc registerUser(service: UserService, user: User) = service.repository.save(user) proc createUserService(repository: UserRepository): UserService = return UserService(repository: repository) """ * **Don't Do This:** Create dependencies directly within components. This makes testing and reuse more difficult, as it requires mocking global objects. """nim # Bad: Hardcoded dependency type UserService = object repository: UserRepository = UserRepository() # Hardcoded dependency proc registerUser(service: UserService, user: User) = service.repository.save(user) """ * **Why:** Dependency injection improves testability, maintainability, and reusability. It allows you to easily replace dependencies with mocks or different implementations. ## 2. Component Types ### 2.1. Pure Functions * **Do This:** Favor pure functions whenever possible. Pure functions have no side effects and always return the same output for the same input. """nim # Good: Pure function proc add(a, b: int): int = # No side effect, returns the same output for same input return a + b """ * **Don't Do This:** Use functions with side effects when a pure function would suffice. Side effects can make code harder to understand and test. """nim # Bad: Function with side effect var sum: int = 0 proc addToSum(a: int) = sum += a # Side effect: modifies global variable """ * **Why:** Pure functions are predictable, testable, and easy to reason about. They can be easily parallelized and optimized by the compiler and the runtime. ### 2.2. Objects and Classes * **Do This:** Use objects and classes to encapsulate data and behavior. """nim # Good: Object with encapsulated data and behavior type Person* = object name*: string age*: int proc greet*(person: Person) = echo "Hello, my name is " & person.name """ * **Don't Do This:** Use global variables or unstructured data when objects or classes would provide better organization and encapsulation. """nim # Bad: Unstructured data var personName: string personAge: int """ * **Why:** Objects and classes provide a powerful way to organize code and data, promoting modularity, reusability, and maintainability. Nim's object system is lightweight and efficient. ### 2.3. Modules * **Do This:** Use modules to group related components and functions. """nim # Good: Module organization (mymodule.nim) module mymodule proc myfunction() = echo "This is my function" """ * **Don't Do This:** Put unrelated components in the same module, or create monolithic modules with too much code. """nim # Bad: Monolithic module module everything proc functionA() proc functionB() proc functionC() """ * **Why:** Modules help to organize code into logical units. Makes dependencies more explicit and improves code discoverability. ### 2.4. Generics * **Do This:** Use generics to create reusable components that work with multiple types. """nim # Good: Generic function proc identity*[T](x: T): T = return x echo identity[int](5) echo identity[string]("hello") """ * **Don't Do This:** Duplicate code for different types when generics can be used. """nim # Bad: Code duplication proc identityInt(x: int): int = return x proc identityString(x: string): string = return x """ * **Why:** Generics reduce code duplication and improve code reusability. They allow you to write components that can work with multiple types without sacrificing type safety. ### 2.5 Concepts (Type Classes) * **Do This:** Leverage concepts to define requirements for generic types. """nim # Good: Using concept to constrain generic type concept Summable[T]: T + T is T proc sum*[T: Summable](a, b: T): T = return a + b echo sum(1, 2) # Works for integers #echo sum("hello", "world") # Compile error as Strings are not Summable """ * **Don't Do This:** Avoid defining generic behavior without proper constraints leading to unexpected errors at runtime. * **Why:** Concepts provide type-safe genericity enabling compile-time checking and better code clarity. They offer a powerful mechanism to define abstract interfaces and enforce type constraints, leading to more robust and maintainable code. ## 3. Design Patterns ### 3.1. Factory Pattern * **Do This:** Use the factory pattern to abstract object creation. """nim # Good: Factory pattern type Animal = interface proc makeSound() {.abstract.} type Dog = object of Animal proc makeSound() {.override.} = echo "Woof!" type Cat = object of Animal proc makeSound() {.override.} = echo "Meow!" proc createAnimal(animalType: string): Animal = case animalType of "dog": return Dog() of "cat": return Cat() else: raise newException(ValueError, "Invalid animal type") let animal = createAnimal("dog") animal.makeSound() """ * **Don't Do This:** Directly instantiate objects in client code, especially when the object creation logic is complex or when the concrete type needs to be hidden. """nim # Bad: Direct instantiation let animal = Dog() animal.makeSound() """ * **Why:** The factory pattern decouples object creation from client code, making the code more flexible and extensible. ### 3.2. Observer Pattern * **Do This:** Use the observer pattern to notify multiple dependent objects when the state of a subject object changes. This can be elegantly accomplished in Nim using closures. """nim # Good: Observer pattern with closures type Subject = object observers: seq[proc (message: string)] proc attach(subject: var Subject, observer: proc (message: string)) = subject.observers.add(observer) proc detach(subject: var Subject, observer: proc (message: string)) = for i, obs in subject.observers: if obs == observer: subject.observers.del(i) break proc notify(subject: Subject, message: string) = for observer in subject.observers: observer(message) var subject: Subject subject.observers = @[] let observer1 = proc (message: string) = echo "Observer 1 received: " & message let observer2 = proc (message: string) = echo "Observer 2 received: " & message subject.attach(observer1) subject.attach(observer2) subject.notify("Hello, observers!") # Output: Observer 1 received: Hello, observers! \n Observer 2 received: Hello, observers! subject.detach(observer2) subject.notify("Second Message") # Output: Observer 1 received: Second Message """ * **Don't Do This:** Directly couple subject objects to dependent objects. This creates tight dependencies and makes it difficult to add or remove observers. * **Why:** Enables a one-to-many dependency relationship without tight coupling. Useful for implementing event handling, reactive systems, and publish-subscribe architectures. **Technology note:** Nim's first class closures make implementing observers straightforward. ### 3.3. Strategy Pattern * **Do This:** Use the strategy pattern to define a family of algorithms, encapsulate each one, and make them interchangeable. """nim # Good: Strategy pattern type SortingStrategy = interface proc sort(data: var seq[int]) {.abstract.} type BubbleSort = object of SortingStrategy proc sort(data: var seq[int]) {.override.} = # Bubble sort implementation type QuickSort = object of SortingStrategy proc sort(data: var seq[int]) {.override.} = # Quicksort implementation type Sorter = object strategy: SortingStrategy proc sort(sorter: Sorter, data: var seq[int]) = sorter.strategy.sort(data) """ * **Don't Do This:** Hardcode algorithms within client code, or use conditional statements to select between algorithms. """nim # Bad: Hardcoded algorithm proc sort(data: var seq[int], algorithm: string) = case algorithm of "bubble": # Bubble sort implementation of "quick": # Quicksort implementation """ * **Why:** Allows you to switch between algorithms at runtime without modifying client code. Promotes flexibility and reusability in algorithm selection. ## 4. Error Handling ### 4.1. Exceptions * **Do This:** Use exceptions to handle exceptional or unexpected situations. Catch exceptions at the appropriate level of abstraction. """nim # Good: Exception handling proc divide(a, b: int): float = if b == 0: raise newException(DivByZeroError, "Division by zero") return float(a) / float(b) try: let result = divide(10, 0) echo result except DivByZeroError: echo "Cannot divide by zero" """ * **Don't Do This:** Ignore exceptions or use them for normal control flow. """nim # Bad: Ignoring exceptions try: let result = divide(10, 0) echo result except: discard # Ignoring the exception """ * **Why:** Exceptions provide a structured way to handle errors and prevent program crashes. Properly handled exceptions improve the robustness and reliability of the system. ### 4.2. Result Types * **Do This:** Use "Result[T, E]" (typically from "std/options") to represent functions that may either return a value of type "T" or an error of type "E". This is the preferred way to handle recoverable errors. """nim import std/options proc parseInt(s: string): Result[int, string] = try: return Ok(parseInt(s)) except ValueError: return Err("Invalid integer format") let result = parseInt("123") case result of Ok(value): echo "Parsed value: ", value of Err(message): echo "Error parsing integer: ", message """ * **Don't Do This:** Rely solely on exceptions for all error scenarios, particularly expected failure conditions. "Result" types provide better compile-time safety and explicit error handling. * **Why:** "Result" types force the caller to handle the potential error case, improving code robustness. They provide a clear and explicit way to represent functions that can fail. They also improve performance as exceptions are costly. ## 5. Documentation ### 5.1. Code Comments * **Do This:** Add comments to explain complex logic, non-obvious code, and the purpose of components. """nim # Good: Meaningful comment proc calculateArea(width, height: int): int = # Calculate the area of a rectangle return width * height """ * **Don't Do This:** Add redundant comments that simply restate the code, or leave code uncommented. """nim # Bad: Redundant comment proc calculateArea(width, height: int): int = # Multiply width and height return width * height """ * **Why:** Code comments are essential for understanding and maintaining code. They provide valuable context for developers who are reading or modifying the code. ### 5.2. API Documentation * **Do This:** Use Nim's documentation generator to create API documentation for components. """nim # Good: API documentation (using doc comments) ## Calculates the area of a rectangle. ## Args: ## width: The width of the rectangle. ## height: The height of the rectangle. ## Returns: ## The area of the rectangle. proc calculateArea*(width, height: int): int = return width * height """ * Run "nim doc yourModule.nim" to generate documentation. Typically "nimble doc" is used for projects. * **Don't Do This:** Neglect to document public APIs, or provide incomplete or inaccurate documentation. * **Why:** API documentation allows others to understand and use your components correctly without having to dive into the implementation details. ## 6. Tooling and Conventions ### 6.1. Compiler Flags * **Do This:** Use appropriate compiler flags to ensure code quality. For example, "-w:error" turns warnings into errors to enforce stricter code standards. Consistent use of a ".nimble" file to specify compiler flags is very useful for project-wide configuration. """nim #Example in .nimble file: requires "nim >= 1.6.0" backend = "cpp" flags="-w:error" """ * **Don't Do This:** Ignore compiler warnings or use inconsistent compiler settings. * **Why:** Using consistent compiler flags improves code quality and helps catch potential errors early in the development process. Turning warnings into errors promotes very high standards. ### 6.2. Code Formatting * **Do This:** Follow a consistent code formatting style. "nimpretty" can automatically format code to a consistent standard. * **Don't Do This:** Use inconsistent formatting, or mix different styles within the same codebase. * **Why:** Consistent code formatting makes the code easier to read and understand. Automation minimizes debates about coding styles. ### 6.3. Linting * **Do This:** Employ a linter (like "nim check") to identify potential code style and semantic issues. Incorporating linting into the build process automates consistent validation. Several linters can be integrated with IDEs providing real-time feedback. * **Don't Do This:** Neglect static analysis, thus allowing common errors and style inconsistencies to go undetected. * **Why:** Linters identify potential issues early, reducing debugging time and improving overall code quality. Automated enforcement reduces the chance of regressions. ## 7. Performance Considerations ### 7.1. Memory Management * **Do this:** Understand Nim’s memory management options (GC, ARC, manual). Choose the best option for the target application (e.g., GC for general use, ARC for deterministic cleanup, manual for full control in performance-critical code). Use "{.gcsafe.}" and "{.noGc.}" pragmas appropriately. """nim #Example of using ARC: {.push gc: arc .} proc processData(data: seq[int]): seq[int] = # Complex processing logic result = newSeq[int](data.len) for i in 0..data.len - 1: result[i] = data[i] * 2 {.pop.} """ * **Don't Do This:** Ignore memory management, assuming the garbage collector (GC) will handle everything optimally. This can lead to memory leaks or performance bottlenecks. * **Why:** Understanding and correctly applying memory management leads to resource-efficient code, minimizing memory usage and avoiding performance degradation. Modern Nim projects increasingly favor ARC for its deterministic cleanup. ### 7.2. Data Structures * **Do This:** Select the most appropriate data structure for the task. Use sequences ("seq") for dynamic arrays, arrays for fixed-size collections, sets ("HashSet") for membership testing, and tables ("Table") or ordered Tables ("OrderedTable") for key-value storage. * **Don't Do This:** Use inappropriate data structures, leading to inefficient operations (e.g., searching a sequence when a set would be more efficient). * **Why:** Efficient data structure usage is crucial for high-performance applications. The appropriate data structure drastically cuts down on processing time. ### 7.3. Iteration * **Do This:** Use iterators for efficient access to collections, especially when transforming data. Consider using parallel iterators ("parallel") for computationally intensive loops. Optimize loops by minimizing computations within loop bodies and using inlining where appropriate. """nim # Example: Efficient iteration and transformation iterator doubledValues(data: seq[int]): int = for item in data: yield item * 2 for value in doubledValues(@[1, 2, 3, 4, 5]): echo value # Output: 2 4 6 8 10 """ * **Don't Do This:** Overlook efficient iteration techniques, performing unnecessary operations within loops. * **Why:** Efficient iteration optimizes data processing and memory access, leading to substantial performance gains, especially in computationally intensive routines. Leverage Nim's powerful iterator features when possible. The "parallel" iterator can unlock significant performance improvement on multi-core hardware. This coding standards document provides guidance for creating high-quality, maintainable, and efficient component designs for Nim. By following these standards, developers can produce code that is easier to understand, test, and reuse.
# State Management Standards for Nim This document outlines the standards for managing state within Nim applications. Effective state management is crucial for building maintainable, scalable, and testable software. It covers different state management approaches applicable to Nim based on the specific needs of an application, including immutability, mutable state with controlled access, and reactive programming. The document emphasizes modern Nim features and preferred patterns. ## 1. Principles of Effective State Management ### 1.1. Immutability **Definition:** An immutable object's state cannot be modified after it is created. Any operation that *appears* to modify it must instead create a new object. **Why it matters:** * **Predictability:** Immutable state simplifies reasoning about code because the value of an object is fixed. * **Thread safety:** Immutable objects are inherently thread-safe. * **Testability:** Easier to test because you don't have to track state changes. * **Caching:** Immutable objects are great for caching, as their validity never changes. **Do This:** * Design data structures as immutable whenever practical. **Don't Do This:** * Unnecessarily mutate state when immutability provides similar functionality. * Share mutable state without careful consideration of concurrency implications. **Example:** """nim type Point = tuple[x, y: int] # tuples are immutable proc movePoint(p: Point, dx, dy: int): Point = ## Returns a new point that is "p" translated by dx, dy. (x: p.x + dx, y: p.y + dy) # creates a new tuple let p1: Point = (x: 1, y: 2) p2 = movePoint(p1, 3, 4) echo p1 # (x: 1, y: 2) echo p2 # (x: 4, y: 6) """ ### 1.2. Controlled Mutability **Definition:** When mutable state is necessary, access to that state should be carefully controlled via well-defined interfaces. **Why it matters:** * **Encapsulation:** Prevents unintended side effects. * **Maintainability:** Makes it easier to understand and modify code. * **Concurrency:** Can be used to manage access to shared resources. **Do This:** * Encapsulate mutable state within objects or modules. * Provide methods or procedures to safely modify state. * Use locks or other synchronization primitives when shared mutable state is accessed by multiple threads. **Don't Do This:** * Expose mutable state directly. * Modify state in unrelated or distant parts of the code. **Example:** """nim import locks type Counter = object value: int lock: Lock proc newCounter(): Counter = Counter(value: 0, lock: newLock()) proc increment(c: var Counter) = acquire(c.lock) try: c.value += 1 finally: release(c.lock) proc getValue(c: Counter): int = acquire(c.lock) try: return c.value finally: release(c.lock) var counter = newCounter() # Example usage within multiple threads import os, threadpool proc threadFunc() = for i in 0..<1000: increment(counter) var threads: array[3, Thread[void]] for i in 0..<threads.len: createThread(threads[i], threadFunc) for i in 0..<threads.len: joinThread(threads[i]) echo "Final value: ", getValue(counter) """ ### 1.3. Explicit Data Flow **Definition:** Make data dependencies clear and predictable. **Why it matters:** * **Readability:** Easier to understand how data is transformed and used. * **Debugging:** Simplifies tracing data through the application. * **Testability:** Makes it easier to isolate units of code for testing. **Do This:** * Pass data explicitly to procedures and functions. * Avoid global variables where possible. * Use clear and descriptive names for variables and parameters. **Don't Do This:** * Rely on implicit state or hidden dependencies. * Use global variables excessively. **Example:** """nim proc calculateArea(width, height: float): float = ## Calculates the area of a rectangle. width * height let w = 10.0 h = 5.0 area = calculateArea(w, h) echo "Area: ", area """ ### 1.4. Reactive Programming (if applicable) **Definition:** A declarative programming paradigm concerned with data streams and the propagation of change. **Why it matters:** * **Responsiveness:** Allows applications to react quickly to changes in data. * **Concurrency:** Can simplify asynchronous programming. * **Declarative style:** Makes it easier to reason about complex interactions. **Do This:** * Consider using reactive libraries like "chronos" or "karax" for UI development. * Use signals or observables to represent data streams. * Use transformations to process and combine data streams. **Don't Do This:** * Overuse reactive programming in simple situations. * Create complex dependency chains that are difficult to debug. **Example using "chronos":** In this example, "chronos" is primarily used for asynchronous operations and its core reactive capabilities not demonstrated. Since Nim doesn't yet have a dominant reactive programming library equivalent to RxJS or similar, a complete reactive example is more involved and would require building custom primitives. """nim import chronos proc main() {.async.} = echo "Starting..." await sleepAsync(1000) # block for 1 second echo "Done!" waitFor main() """ ## 2. Managing Application State ### 2.1. Centralized State **Definition:** Storing all application state in a single, well-defined location, typically an object or module. **Why it matters:** * **Organization:** Provides a clear view of the application's overall state. * **Maintainability:** Simplifies debugging and modification of state. * **Testability:** Makes it easier to set up application-wide tests. **Do This:** * Create a central state object or module. * Define clear accessors and mutators for the state. **Don't Do This:** * Scatter state throughout the application. * Allow direct modification of the state from multiple locations. **Example:** """nim type AppState = object username: string isLoggedIn: bool items: seq[string] var appState: AppState proc initializeState() = appState = AppState(username: "", isLoggedIn: false, items: @[]) proc login(username: string) = appState.username = username appState.isLoggedIn = true proc logout() = appState.username = "" appState.isLoggedIn = false proc addItem(item: string) = appState.items.add(item) proc getItem(index: int): string = if index >= 0 and index < appState.items.len: return appState.items[index] else: return "" initializeState() login("testuser") addItem("item1") echo appState.username echo appState.items """ ### 2.2. State Monads **Definition:** Using a monad to encapsulate and manage state transformations in a functional style. This is more advanced. **Why it matters:** * **Purity:** Makes state transformations explicit and isolated. * **Composability:** Allows complex state transformations to be built from simpler ones. * **Testability:** Each transformation becomes easily testable. **Do This:** * Define a state monad type. * Create helper functions to access and modify the state. * Use "bind" or similar operations to chain state transformations. **Don't Do This:** * Overuse state monads in simple cases. * Create overly complex monad stacks. **Example:** A full State Monad example is a complex topic, better suited to a larger project demonstrating more elaborate transformations. The following is a simplified demonstration of the concepts. Since Nim doesn't have direct syntactic sugar for monads many steps have to be explicitly coded. """nim type State[S, A] = proc (state: S): (A, S) # Represents a state transformation # S is state type, A is returned computation type proc runState[S, A](state: State[S, A], initialState: S): (A, S) = state(initialState) proc unit[S, A](value: A): State[S, A] = proc (state: S): (A, S) = (value, state) proc bind[S, A, B](state: State[S, A], f: proc (a: A): State[S, B]): State[S, B] = proc (s: S): (B, S) = let (a, newState) = state(s) f(a)(newState) # Example: Increment a counter using a state monad type CounterState = int proc getCounter: State[CounterState, CounterState] = proc (state: CounterState): (CounterState, CounterState) = (state, state) # Returns the current state and keeps the state unchanged proc setCounter(newValue: CounterState): State[CounterState, void] = proc (state: CounterState): (void, CounterState) = ((), newValue) # Returns void (unit type) and sets the counter to a new state proc incrementCounter: State[CounterState, void] = bind[CounterState, CounterState, void](getCounter, proc (counter: CounterState): State[CounterState, void] = setCounter(counter + 1) ) # Usage let initialState: CounterState = 0 let ((), finalState) = runState(incrementCounter, initialState) echo "Final counter value: ", finalState """ ### 2.3. Global Variables (Use Sparingly) **Definition:** Variables declared outside of any procedure or object, accessible from anywhere in the code. **Why it matters:** * **Convenience:** Can be easily accessed and modified. * **Performance:** Can avoid the overhead of passing data around. **Don't Do This:** * Global variables should be "const" or "let" whenever possible to prevent accidental modification and improve reasoning. * Use global variables as a primary means of managing application state. * Modify global variables from multiple threads without synchronization. * Global variables with mutable value types are almost always a bad idea, and should be replaced with explicit access-controlled types. **When to use:** * Configuration settings. * Constants that are used throughout the application. * Logging configurations. **Example:** """nim const appName = "MyApplication" version = "1.0.0" var debugMode = false # Only configure here proc log(message: string) = if debugMode: echo appName, " - ", message debugMode = true # Set once during initialization log("Application started") """ ## 3. Data Flow Management ### 3.1. Functional Programming **Definition:** Writing code primarily using pure functions (functions without side effects) and immutable data. **Why it matters:** * **Predictability:** Makes code easier to reason about. * **Testability:** Simplifies unit testing. * **Concurrency:** Eliminates the need for locks and other synchronization primitives. **Do This:** * Use pure functions whenever possible. * Avoid modifying data directly. * Use recursion instead of loops when appropriate. * Leverage Nim's "seq.map", "seq.filter", "seq.foldl" and "seq.foldr" for data processing. **Don't Do This:** * Write functions with side effects that are not clearly documented. * Mutate data unnecessarily. **Example:** """nim proc square(x: int): int = ## Returns the square of x. x * x let numbers = @[1, 2, 3, 4, 5] squares = numbers.map(square) # creates a new sequence with mapped result echo squares # @[1, 4, 9, 16, 25] """ ### 3.2. Message Passing **Definition:** Communicating between different parts of the application by sending and receiving messages. Commonly used for concurrency. **Why it matters:** * **Decoupling:** Reduces dependencies between components. * **Concurrency:** Simplifies parallel programming. * **Resilience:** Improves fault tolerance. **Do This:** * Define clear message formats. * Use channels or queues to send and receive messages. * Handle errors gracefully. **Don't Do This:** * Send large amounts of data in messages. * Block indefinitely waiting for messages. **Example:** """nim import channels import os, threadpool type Message = object text: string var channel: Channel[Message] proc worker() = while true: let message = recv(channel) echo "Received: ", message.text proc main() = channel = newChannel[Message]() var workerThread: Thread[void] createThread(workerThread, worker) send(channel, Message(text: "Hello from main thread!")) send(channel, Message(text: "Another message")) close(channel) # Signal end of communication joinThread(workerThread) main() """ ### 3.3. Data Transformations **Definition:** Emphasizing transformations of data through a series of steps. **Why it matters:** * **Clarity:** Each transformation step is explicit. * **Debuggability:** Easier to track the flow of data. * **Testability:** Individual transformations can be tested in isolation. **Do This:** * Break down complex operations into smaller, well-defined transformations. * Use immutable data structures where appropriate. * Utilize pipelines to chain transformations together. **Don't Do This:** * Create overly complex transformation chains. * Modify data in place during transformations. **Example:** """nim import strutils proc toUpper(s: string): string = ## Converts a string to uppercase. s.toUpperAscii() proc trim(s: string): string = ## Trims whitespace from a string. s.strip() proc addExclamation(s: string): string = ## Adds an exclamation point to a string. s & "!" let message = " hello world " transformedMessage = addExclamation(trim(toUpper(message))) echo transformedMessage """ ## 4. Reactivity and Event Handling ### 4.1. Callbacks **Definition:** Using procedures that are called when specific events occur. **Why it matters:** * **Simple:** Easy to implement for basic event handling. **Do This:** * Use callbacks for simple event handling scenarios. * Consider more advanced mechanisms for complex interactions. **Don't Do This:** * Overuse callbacks leading to callback hell. * Perform long-running operations within callbacks. **Example:** """nim type Button = object onClick: proc () proc createButton(clickAction: proc ()): Button = Button(onClick: clickAction) proc handleClick(button: Button) = button.onClick() proc myCallback() = echo "Button clicked!" let button = createButton(myCallback) handleClick(button) """ ### 4.2. Signals and Slots (Similar to Qt Framework) **Definition:** A mechanism for connecting objects, where a signal emitted by one object triggers a slot (procedure) in another object. Nim does not have built-in support for signals and slots, so this needs to be implemented or a library used **Why it matters:** * **Decoupling:** Reduces dependencies between emitting and receiving objects. * **Flexibility:** Allows multiple slots to be connected to a single signal. * **Extensibility:** Makes it easy to add new event handlers. **Do This:** * Define signal types. * Create procedures to emit signals. * Create procedures as slots to handle signals. * Implement a connection mechanism to link signals and slots. **Don't Do This:** * Create circular dependencies between signals and slots. * Perform long-running operations in slots. **Example (basic implementation):** """nim type Signal = ref object listeners: seq[proc ()] proc newSignal(): Signal = Signal(listeners: @[]) proc connect(signal: Signal, listener: proc ()) = signal.listeners.add(listener) proc emit(signal: Signal) = for listener in signal.listeners: listener() # Example Usage var mySignal = newSignal() proc mySlot() = echo "Signal received!" connect(mySignal, mySlot) # connecting a procedure (mySlot) to a signal emit(mySignal) # Will trigger outputs from any attached slots """ ### 4.3. Message Queues (For Asynchronous Communication) **Definition:** Using a queue to store and process messages asynchronously. *Suitable for cases where responsiveness is key.* **Why it matters:** * **Decoupling:** Allows components to communicate without direct dependencies. * **Scalability:** Enables asynchronous processing of events. * **Resilience:** Improves fault tolerance. **Do This:** * Define message types. * Use a queue to store incoming messages. * Use a separate thread or process to process messages from the queue. **Don't Do This:** * Lose messages due to queue overflow. * Block indefinitely waiting for messages. ## 5. Error Handling ### 5.1. Exceptions **Definition:** Using exceptions to signal errors. **Do This:** * Use exceptions to signal unexpected errors that cannot be handled locally. * Catch exceptions at a higher level to handle errors gracefully. * Use "try...except...finally" blocks to ensure proper cleanup. **Don't Do This:** * Use exceptions for normal control flow. * Ignore exceptions without handling them. * Catch exceptions too broadly. **Example:** """nim proc divide(x, y: int): float = if y == 0: raise newException(CatchableError, "Division by zero") return float(x) / float(y) try: let result = divide(10, 0) echo "Result: ", result except DivisionByZeroError: echo "Error: Cannot divide by zero" finally: echo "Finished division attempt." """ ### 5.2. Result Types **Definition:** Using a type that explicitly represents either a successful value or an error. **Why it matters:** * **Explicit:** Forces the caller to handle the possibility of an error. * **Safe:** Prevents unexpected exceptions. * **Functional:** Fits well with functional programming paradigms. **Do This:** * Define a result type with variants for success and failure. * Return a result type from functions that can fail. * Use case statements or pattern matching to handle results. **Don't Do This:** * Ignore the result of a function that returns a result type. * Throw exceptions after using Result type patterns. **Example:** """nim type Result[T, E] = object case kind: enum {ok, err} of ok: value: T of err: error: E proc divide(x, y: int): Result[float, string] = if y == 0: return Result[float, string](kind: err, error: "Division by zero") else: return Result[float, string](kind: ok, value: float(x) / float(y)) let result = divide(10, 0) case result.kind of ok: echo "Result: ", result.value of err: echo "Error: ", result.error """ ## 6. Tooling and Libraries. ### 6.1. Testing Frameworks * Use a testing framework such as "unittest" to write unit tests. * Write tests for all critical code paths. * Use mocking libraries to isolate units of code for testing. ### 6.2. Debugging Tools * Use a debugger to step through code and inspect variables. The Nim compiler has GDB integration. * Use logging to record information about the execution of the application. ## 7. Security Considerations ### 7.1. Input Validation * Validate all input data to prevent security vulnerabilities. * Use appropriate encoding and escaping techniques to prevent injection attacks. ### 7.2. Data Encryption * Encrypt sensitive data both in transit and at rest. * Use strong encryption algorithms and key management practices. ### 7.3. Access Control * Implement appropriate access controls to protect sensitive data and resources. * Use the principle of least privilege. ## 8. Conclusion Effective State Management is critical for the development of robust and maintainable Nim applications. By adhering to these standards and guidelines, developers can create well-structured, secure, and performant code. The correct approach depends significantly on the complexity of the specific application being developed. The principles outlined above should, however, always be a consideration.
# Code Style and Conventions Standards for Nim This document outlines the coding style and conventions for Nim, aiming to ensure code readability, maintainability, and consistency across projects. Following these guidelines helps facilitate collaboration, reduces cognitive load, and improves the overall quality of the Nim codebase. These standards are designed with the latest Nim features and best practices in mind. ## I. Formatting Consistent formatting is crucial for readability. Nim's flexibility allows for various styles, but adhering to a standard improves code comprehension. ### A. Indentation * **Do This:** Use 2 spaces for indentation. This improves readability without overly expanding code blocks. * **Don't Do This:** Use tabs or more than 2 spaces. Tabs render differently across editors, and excessive indentation obscures code structure. * **Why:** Consistent indentation accurately reflects code hierarchy. Misaligned or inconsistent indentation can lead to misinterpretations and bugs. """nim proc calculateArea(width, height: int): int = let area = width * height return area """ ### B. Line Length * **Do This:** Limit lines to a maximum of 120 characters. This keeps code readable on various screen sizes and encourages breaking down long expressions into smaller, more manageable parts. * **Don't Do This:** Exceed the line length limit without good reason. Horizontal scrolling reduces readability. * **Why:** Improves readability and printability. Prevents long lines from wrapping awkwardly in editors and code review tools. """nim # Good: Fits within the line length limit let result = calculateSomethingComplex(arg1, arg2, arg3, arg4) # Better: Breaks down the long line let intermediateResult = calculateSomethingElse(arg1, arg2) result = calculateSomethingComplex(intermediateResult, arg3, arg4) """ ### C. Whitespace * **Do This:** * Use a single blank line to separate logical blocks of code (e.g., between procedures, after type definitions). * Use a space after commas in argument lists. * Use spaces around operators (e.g., "x + y", "a = b"). * Use spaces around colons in type declarations (e.g., "x: int"). * **Don't Do This:** * Overuse blank lines, creating excessively sparse code. * Omit spaces around operators, reducing readability. * **Why:** Consistent whitespace enhances readability by visually separating code elements. """nim # Good type Person = object name: string age: int proc greet(person: Person): string = return "Hello, " & person.name # Bad - hard to read due to lack of whitespace type Person=object name:string age:int proc greet(person:Person):string= return"Hello,"&person.name """ ### D. Parentheses * **Do This:** Use parentheses to clarify precedence, especially in complex expressions. It is preferred even when not strictly required. * **Don't Do This:** Omit parentheses in situations where precedence could be ambiguous. * **Why:** Parentheses reduce ambiguity and prevent operator precedence errors. """nim # Good - clear precedence let result = a + (b * c) # Bad - precedence uncertain let result = a + b * c """ ### E. File Structure * **Do This:** Organize files logically. Keep related types, procedures, and constants in the same module. * **Don't Do This:** Create monolithic files with unrelated code or scatter related code across multiple modules unnecessarily. * **Why:** Logical file structure improves code discoverability and maintainability. #### Example: Module Layout """nim # mymodule.nim type MyType = object field1: int field2: string const MY_CONSTANT = 123 proc doSomething(x: MyType): int = # ... implementation ... return 0 proc doSomethingElse(y: string): string = # ... implementation ... return y """ ## II Naming Conventions Clear and consistent naming significantly improves code maintainability. ### A. General Principles * **Do This:** Use descriptive names that clearly indicate the purpose of a variable, function, or type. * **Don't Do This:** Use single-letter variable names (except in very short loops), cryptic abbreviations, or names that don't accurately reflect the entity's purpose. * **Why:** Descriptive names reduce cognitive load and make code self-documenting. ### B. Identifiers * **Modules:** Use PascalCase (e.g., "MyModule"). * **Types:** Use PascalCase (e.g., "Person", "Address"). * **Constants:** Use SCREAMING_SNAKE_CASE (e.g., "MAX_RETRIES", "DEFAULT_TIMEOUT"). * **Variables:** Use camelCase (e.g., "userName", "totalCount"). * **Procedures/Functions:** Use camelCase (e.g., "calculateArea", "getUserName"). * **Parameters:** Use camelCase (e.g., "firstName", "accountBalance"). * **Enums:** Use PascalCase for the enum type itself, and PascalCase for the enum values (e.g., "Color", "Red", "Green", "Blue"). #### Example: Naming Styles """nim type Person = object # PascalCase for types firstName: string # camelCase for variables/parameters lastName: string const MAX_AGE = 150 # SCREAMING_SNAKE_CASE for constants proc getFullName(person: Person): string = # camelCase for procedures return person.firstName & " " & person.lastName """ ### C. Boolean Variables and Procedures * **Do This:** Name boolean variables and procedures to clearly indicate a boolean value that answers a positive/negative question.. Prefix boolean variable names with "is", "has", or "can" (e.g., "isLoggedIn", "hasPermission", "canAccess"). * **Don't Do This:** Use ambiguous names or names that suggest the opposite meaning. * **Why:** Improves readability and reduces confusion about the meaning of boolean values. """nim var isLoggedIn: bool # Good proc isValidUser(username, password): bool = # Good # ... implementation ... return true """ ### D. Avoid Shadowing * **Do This:** Use distinct names for variables and parameters to avoid shadowing. * **Don't Do This:** Define a variable with the same name as an outer-scoped variable or parameter. * **Why:** Shadowing can lead to subtle bugs and make code harder to understand. """nim proc processData(data: string): string = let localData = data.toUpperAscii() # Use a different name 'localData' return localData """ ### E. Generic Type Parameters * **Do This:** Use single uppercase letters (T, U, V, etc.) for generic type parameters. * **Why:** This is the established convention in Nim and many other languages. """nim proc identity[T](x: T): T = return x """ ### F. Imports * **Do This:** Use "import" for most modules. For modules you use frequently, consider "include" for shorter code, knowing it pollutes the namespace. Use "from X import Y" sparingly to avoid namespace pollution but improve readability when repeatedly accessing the same member. * **Don't Do This:** Overuse "include" to an extent that makes it unclear where symbols are defined. Avoid wildcard imports ("from X import *"). * **Why:** Controlled imports improve code clarity and prevent naming conflicts, leading to more maintainable project structures. """nim import strutils # Import the entire module from os import getEnv # Import only a specific function echo getEnv("PATH") # No need to prefix with "os." echo toUpperAscii("hello") # Requires strutils.toUpperAscii if not included """ ## III. Stylistic Consistency Consistency in code style improves code readability and reduces cognitive load. ### A. Control Flow * **Do This:** Use consistent indentation and spacing within "if", "for", "while", and "case" statements. * **Don't Do This:** Mix different styles or omit braces/"end" inappropriately. * **Why:** Consistent formatting makes the code easier to follow and reduces the risk of syntax errors. """nim # Good if condition: statement1 statement2 else: statement3 # Good - single-line if/else if condition: statement1 else: statement2 # Conserved style with braces - still good, but less idiomatic if condition: { statement1 statement2 } else: { statement3 } """ """nim # Good example of complex if/elif/else if temperature > 30: echo "It's hot outside!" elif temperature > 20: echo "It's a pleasant day." else: echo "It's a bit chilly." """ ### B. Case Statements * **Do This:** Align "of" clauses in "case" statements. * **Don't Do This:** Misalign clauses, making the code harder to read. * **Why:** Alignment enhances readability and makes it easier to visually scan the different cases. """nim # Good case fruit of "apple": echo "It's an apple" of "banana": echo "It's a banana" else: echo "Unknown fruit" """ ### C. String Formatting * **Do This:** Use the "fmt" module for string formatting in most cases. It's more readable and type-safe than manual concatenation. Alternatively, consider using the $"" string interpolation. * **Don't Do This:** Overuse manual string concatenation, especially with multiple variables. * **Why:** "fmt" improves readability and reduces the risk of errors associated with manual string formatting. """nim import strformat let name = "Alice" let age = 30 # Good: Using strformat let message = &"Hello, {name}! You are {age} years old." echo message # Good: String Interpolation let message2 = $"Hello, {name}! You are {age} years old." echo message2 # Okay - Simple concatenation let message3 = "Hello, " & name & "! You are " & $age & " years old." # Explicit casting to string for age is required echo message3 """ ### D. Error Handling * **Do This:** Use exceptions or the "Result[T, E]" type for error handling. Choose the appropriate method based on the context. * **Don't Do This:** Ignore errors or use error codes without proper handling. * **Why:** Robust error handling prevents unexpected program termination and provides informative error messages. """nim # Using exceptions proc divide(a, b: int): int = if b == 0: raise newException(ValueError, "Cannot divide by zero") return a div b # Using Result type (preferred for functions where failure is expected) import results proc divideSafe(a, b: int): Result[int, string] = if b == 0: return err("Cannot divide by zero") else: return ok(a div b) """ ### E. Comments * **Do This:** Write clear and concise comments to explain complex logic, non-obvious algorithms, or the purpose of a section of code. Use comments judiciously; code should be self-explanatory wherever possible. Comments should explain *why* the code does something, not *what* it does (the code already shows that). * **Don't Do This:** Write redundant comments that merely restate the code. Don't omit comments for complex or critical sections of code. * **Why:** Comments enhance code understanding and facilitate maintenance. """nim # This procedure calculates the discounted price. # Discount is applied only if the customer is a member. proc calculateDiscountedPrice(price: float, isMember: bool): float = if isMember: # Apply 10% discount for members return price * 0.9 else: return price """ ### F. Documentation Strings ("##") * **Do This:** Use documentation strings ("##") to document public types, procedures, and modules. * **Don't Do This:** Omit documentation for public API elements. * **Why:** Documentation strings are used by documentation generators tools and IDEs to provide helpful information to users of your code. """nim ## This module provides utility functions for string manipulation. module strutils2 ## Converts a string to uppercase. ## ## Args: ## s: The input string. ## ## Returns: ## The uppercase version of the string. proc toUpper(s: string): string = # ... implementation ... return s.toUpperAscii() """ ### G. Mutability * **Do This:** Prefer immutable variables ("let") over mutable variables ("var") whenever possible. Use "var" only when the variable's value needs to change. * **Don't Do This:** Use "var" unnecessarily, increasing the risk of unintended side effects. * **Why:** Immutability makes code easier to reason about and reduces the potential for bugs. """nim # Good - value doesn't change let message = "Hello, world!" # Only use var if needed - value will be modified var counter = 0 counter += 1 """ ### H. Pragmas * **Do This:** Use pragmas judiciously to control compiler behavior. Document the reason for using a pragma, especially if it's not obvious. Use curly braces and a dot for pragmas (e.g., "{ .raises: [ValueError] }"). * **Don't Do This:** Overuse pragmas without a clear understanding of their effects. * **Why:** Pragmas can improve performance, enable specific language features, or suppress compiler warnings. """nim proc calculateSquareRoot(x: float): float {.raises: [ValueError].} = if x < 0: raise newException(ValueError, "Cannot calculate square root of negative number") return sqrt(x) """ ### I. Object-Oriented Programming * **Do This:** Use objects and methods when it makes sense to encapsulate data and behavior. Consider value objects when you need a light-weight immutable data container. * **Don't Do This:** Overuse object-oriented programming when simpler approaches like procedures and data types would be more appropriate. * **Why:** OOP can improve code organization and reusability. """nim type Rectangle = object width, height: float proc area(r: Rectangle): float = return r.width * r.height """ ### J. Asynchronous Programming (Async/Await) * **Do This:** Use the "async" and "await" keywords for asynchronous operations. Handle exceptions properly in asynchronous procedures. Use the "asyncCheck" pragma. * **Don't Do This:** Block the main thread with long-running synchronous operations. Ignore exceptions in asynchronous procedures. * **Why:** Asynchronous programming improves application responsiveness and scalability. """nim import asyncdispatch async proc fetchData(url: string): string = # Simulate fetching data from a remote server await sleepAsync(1000) # Simulate network delay return "Data from " & url async proc main() {.asyncCheck.} = let data = await fetchData("https://example.com") echo data waitFor main() """ ### K. Standard Library Usage * **Do This:** Utilize the Nim standard library whenever possible. It is well-tested and optimized. * **Don't Do This:** Reinvent the wheel by writing custom functions for tasks that are already handled by the standard library. * **Why:** The standard library provides a wide range of functions and data structures that can simplify development. """nim import strutils, sequtils let message = "hello world" let upperCaseMessage = message.toUpperAscii() # standard library function let numbers = @[1, 2, 3, 4, 5] let evenNumbers = numbers.filterIt(it mod 2 == 0) # standard library function """ ### L. Iterators and Generators * **Do This:** Use iterators and generators for efficient data processing, especially when dealing with large datasets. * **Don't Do This:** Load entire datasets into memory when iterating or processing small junks of it. * **Why:** Iterators and generators provide a memory-efficient way to process data on demand, reducing memory footprint and improving performance. """nim iterator fibonacci(): int = var a = 0 var b = 1 while true: yield a let temp = a + b a = b b = temp for num in fibonacci(): if num > 100: break echo num """ ### M. Concurrency * **Do This:** Manage concurrency using channels and threads from the "channels" and "threads" modules for safe and efficient concurrent operations. Protect shared mutable resources with locks or atomic operations. * **Don't Do This:** Directly manipulate shared memory without proper synchronization mechanisms, leading to race conditions and data corruption. * **Why:** Safe concurrency helps parallelize tasks and improve performance in multi-core systems. """nim import channels, locks, threads var counter = 0 var lock: Lock proc incrementCounter() = acquire lock try: counter += 1 finally: release lock proc worker(id: int) = for i in 0..<1000: incrementCounter() echo "Worker ", id, " finished" var thread1 = createThread(worker, 1) var thread2 = createThread(worker, 2) joinThread(thread1) joinThread(thread2) echo "Final counter value: ", counter """ Following these code style and convention standards consistently will help ensure the creation of maintainable, readable, and robust Nim codebases.
# Core Architecture Standards for Nim This document outlines the core architecture standards for Nim projects. These guidelines promote maintainability, scalability, performance, and security. They're designed to be used by both human developers and AI coding assistants to ensure consistency and quality across the codebase. ## 1. Architectural Patterns ### 1.1. Layered Architecture **Do This:** * Organize your application into distinct layers, typically: * Presentation Layer (UI, CLI) * Application Layer (Business Logic, Use Cases) * Domain Layer (Entities, Value Objects) * Infrastructure Layer (Database, External Services, I/O) **Why:** Layered architecture decouples concerns, making the application easier to understand, test, and modify. Changes in one layer have minimal impact on others. **Example:** """nim # domain/user.nim type UserID = distinct int User = object id: UserID username: string email: string # application/user_service.nim import domain/user proc createUser(username, email: string): Result[User, string] = ## Creates a new user. if username.len < 3: return err("Username must be at least 3 characters long.") # ... other validation logic ... let newUser = User(id: UserID(hash(username)), username: username, email: email) # ... persist user (infrastructure layer) ... return ok(newUser) # presentation/cli.nim import application/user_service import std/terminal proc run(): void = echo "Enter username:" let username = readLine(stdin) echo "Enter email:" let email = readLine(stdin) case createUser(username, email) of ok(user): echo "User created with ID: ", user.id of err(errorMessage): echo "Error creating user: ", errorMessage run() """ **Don't Do This:** * Avoid tight coupling between layers. Don't let the presentation layer directly access the database. * Don't create God objects that handle multiple responsibilities across layers. ### 1.2. Hexagonal Architecture (Ports and Adapters) **Do This:** * Define clear boundaries around your core domain logic. * Interact with the outside world (databases, external services) through interfaces (ports). * Implement adapters to translate between the interface and the specific technology. **Why:** Hexagonal architecture makes the core domain independent of infrastructure details. Enables easier testing and swapping of external dependencies. **Example:** """nim # core/user_repository.nim (Port) import domain/user type UserRepository = object # Abstract interface proc findUser(repo: UserRepository, id: UserID): Result[User, string] {.raises: [Defect].} proc saveUser(repo: UserRepository, user: User): Result[void, string] {.raises: [Defect].} # infrastructure/postgres_user_repository.nim (Adapter) import core/user_repository import domain/user import std/db_postgres type PostgresUserRepository = object of UserRepository connection: PgConnection proc findUser(repo: PostgresUserRepository, id: UserID): Result[User, string] = # Implementation using Postgres try: let query = "SELECT id, username, email FROM users WHERE id = $1" let result = repo.connection.exec(query, $id) if result.ntuples == 0: return err("User not found.") return ok(User(id: UserID(result.getStr(0, "id").parseInt), username: result.getStr(0, "username"), email: result.getStr(0, "email"))) except PgError as e: return err(e.msg) proc saveUser(repo: PostgresUserRepository, user: User): Result[void, string] = # Implementation using Postgres try: let query = "INSERT INTO users (id, username, email) VALUES ($1, $2, $3)" repo.connection.exec(query, $user.id, user.username, user.email) return ok() except PgError as e: return err(e.msg) # application/user_service.nim import core/user_repository import domain/user proc createUser(repo: UserRepository, username, email: string): Result[User, string] = ## Creates a new user using provided repository let newUser = User(id: UserID(hash(username)), username: username, email: email) let saveResult = repo.saveUser(newUser) if saveResult.isErr(): return saveResult.mapErr(proc(x: string): string = "Failed to save user: " & x) return ok(newUser) """ **Don't Do This:** * Let infrastructure details leak into the core domain. * Create direct dependencies between domain logic and specific database implementations. ### 1.3 Microservices **Do This:** * Decompose large applications into smaller, independent services. Each service should have a specific responsibility. * Communicate between services using well-defined APIs (e.g., REST, gRPC, message queues). * Design each service to be independently deployable and scalable. * Embrace eventual consistency in distributed systems. **Why:** Microservices allow teams to work independently, scale specific parts of the application, and improve fault isolation. **Example:** * A "UserService" for managing user accounts. * A "ProductService" for managing product catalogs. * An "OrderService" for handling order placement and fulfillment. These services would communicate via REST APIs or a message queue like RabbitMQ. A sample UserService endpoint might be: """nim # UserService (simplified) import std/httpclient import std/json proc createUser(username, email: string): JsonNode = # ... create user logic ... result = %* {"id": userId, "username": username, "email": email} # OrderService (simplified) import std/httpclient import std/json proc placeOrder(userId, productId: int): JsonNode = let client = newHttpClient() let userResult = client.get("http://userservice/users/" & $userId) # Assuming a user service if userResult.status != Http200: raise newException(ValueError, "Could not retrieve user information") # ... continue order placing based on the retrieved user data result = %* {"orderId": someOrderId, "userId": userId, "productId": productId} """ **Don't Do This:** * Create tightly coupled services that depend on each other's internal implementation details. * Build a distributed monolith, where services are technically separate but still require coordinated deployments. ## 2. Project Structure and Organization ### 2.1. Package Management with Nimble **Do This:** * Use "nimble" to manage dependencies. * Create a "*.nimble" file for your project. * Specify dependencies clearly with version constraints. * Use "src/", "tests/", and "docs/" directories following the standard convention. **Why:** "nimble" ensures reproducible builds and simplifies dependency management. Consitent directory structures improve readability. **Example:** """nim # myproject.nimble version = "0.1.0" author = "Your Name" description = "A brief description of your project." license = "MIT" requires "nim >= 1.6.0" requires "chronos >= 1.4.0" srcDir = "src" binDir = "bin" testDir = "tests" # dependencies requires "https://github.com/username/other_library.git" """ **Don't Do This:** * Manually download and manage dependencies. * Check in dependencies directly into your repository. ### 2.2. Module Structure **Do This:** * Organize code into logical modules. * Use meaningful module names. * Limit the size of each module to a manageable scope (e.g., a single class or a set of related functions). * Use "import" statements to declare dependencies between modules. * Favor explicit exports to control the module's public API. **Why:** Modules promote code reuse, reduce complexity, and improve maintainability. **Example:** """nim # src/mymodule.nim type MyType* = object # Asterisk makes the type public field1*: int # Also public field2: string # Private proc myFunction*(x: int): int {.exportc.} = # Public & exported for C interop ## A public function. return x * 2 proc privateHelper(y: int): int = ## A private helper function. return y + 1 """ **Don't Do This:** * Create large, monolithic modules that contain unrelated code. * Expose internal implementation details through the public API. ### 2.3. Error Handling Strategy **Do This:** * Use "Result[T, E]" for functions that can fail. "Result" type requires explicit error handling in the caller. * Use exceptions sparingly, primarily for truly exceptional situations (e.g., out-of-memory). Use only for situations where the program *cannot* reasonably recover. * Consider discriminated unions (enums with associated data) for representing different error states. * Log errors with sufficient context for debugging. **Why:** Clear error handling improves the reliability of the application. "Result" types avoid silent failures. **Example:** """nim import std/net proc connectToHost(host: string, port: Port): Result[Socket, string] = try: let socket = newSocket(AF_INET, SOCK_STREAM, IPPROTO_TCP) socket.connect(host, port) return ok(socket) except OSError as e: return err(e.msg) let connectionResult = connectToHost("example.com", Port(80)) case connectionResult of ok(socket): echo "Connected successfully!" socket.close() of err(errorMessage): echo "Connection failed: ", errorMessage """ **Don't Do This:** * Ignore potential errors. * Rely solely on exceptions for all error handling (can make control flow difficult to follow). * Catch exceptions without logging or handling them appropriately. * Use exceptions for routine, expected failures. Handle expected failure states with "Result" or similar constructs. ## 3. Coding Conventions ### 3.1. Naming Conventions **Do This:** * Use camelCase for variables and parameters (e.g., "userName", "orderId"). * Use PascalCase for types and modules (e.g., "User", "OrderService"). * Use SCREAMING_SNAKE_CASE for constants (e.g., "MAX_RETRIES"). * Use descriptive and meaningful names. * Prefix boolean variables with "is", "has", or "can" (e.g., "isLoggedIn", "hasPermission"). **Why:** Consistent naming conventions improve code readability and help developers quickly understand the purpose of different entities. **Example:** """nim const MAX_CONNECTIONS = 10 type User = object userId: int userName: string emailAddress: string isLoggedIn: bool proc getUserById(userId: int): Result[User, string] = # Implementation here using camelCase for variables let userName = "exampleUser" let emailAddress = "user@example.com" return ok(User(userId: userId, userName: userName, emailAddress: emailAddress, isLoggedIn: true)) """ **Don't Do This:** * Use single-letter variable names (except for loop counters). * Use abbreviations that are unclear or ambiguous. * Violate the established naming conventions for Nim. ### 3.2. Immutability **Do This:** * Use "let" for variables that should not be reassigned. * Use immutable data structures when appropriate (e.g., "tuple", "string", "set", "frozenset"). * Avoid modifying data structures in place. Instead, create a copy with the desired modifications. **Why:** Immutability reduces the risk of unexpected side effects and makes code easier to reason about. It can also enable performance optimizations. **Example:** """nim type Point = tuple[x: int, y: int] # Immutable tuple let origin: Point = (x: 0, y: 0) proc movePoint(point: Point, dx, dy: int): Point = ## Returns a *new* Point, because Point is immutable. return (x: point.x + dx, y: point.y + dy) let newPoint = movePoint(origin, 10, 20) echo origin # (x: 0, y: 0) - unchanged echo newPoint # (x: 10, y: 20) """ **Don't Do This:** * Use "var" when "let" is sufficient. * Modify data structures in place when it's not necessary. ### 3.3. Code Formatting **Do This:** * Use 2-space indentation. * Keep lines short (ideally less than 120 characters). * Use blank lines to separate logical blocks of code. * Follow the official Nim style guide (see links at top). **Why:** Consistent code formatting improves readability and makes it easier to collaborate with other developers. **Example:** """nim proc calculateTotalPrice(items: seq[(string, float)], taxRate: float): float = ## Calculates the total price of a list of items with tax. var subtotal = 0.0 for item in items: subtotal += item[1] let taxAmount = subtotal * taxRate return subtotal + taxAmount """ **Don't Do This:** * Use inconsistent indentation. * Write excessively long lines of code. * Omit blank lines between logical blocks. ### 3.4 Concurrency and Parallelism **Do This:** * Use "channels" and "threads" from "std/channels" and "std/threadpool" for concurrent operations. * Use "async" and "await" for asychronous operation using the "asyncdispatch" library. * Minimize shared mutable state. If shared state is unavoidable, use locks or other synchronization primitives to protect it. * Use "spawn" to create lightweight threads ("agents"). * Consider using a task queue or pool for managing concurrent tasks. Why: Using concurrency and parallelism efficiently improves the performance and responsiveness of the application. Following proper guidelines prevents race conditions and deadlocks. Example: """nim # Example using async/await with asyncdispatch import asyncdispatch import std/net import std/strutils # for split async proc handleClient(socket: Socket): Future[void] = defer: socket.close() var buffer: array[4096, char] let bytesRead = await socket.recv(buffer.addr, buffer.len) if bytesRead > 0: let request = $buffer[0..<bytesRead] echo "Received: ", request let parts = request.split("\r\n") let response = "HTTP/1.1 200 OK\r\nContent-Length: 12\r\n\r\nHello World!" await socket.send(response.cstring, response.len) async proc startServer(port: Port): Future[void] = let server = newAsyncSocket() server.bindAddr(port) server.listen() echo "Server listening on port ", port while true: let client = await server.accept() discard handleClient(client) # Detach the handler; no need to await proc main(): void = asyncCheck run(startServer(Port(8080))) # Start the server and check errors main() """ **Don't Do This:** * Share mutable data between threads without proper synchronization. * Create too many threads, as this can lead to excessive context switching and reduced performance. * Block the main thread with long-running operations. * Ignore potential exceptions or errors in concurrent code. ## 4. Testing ### 4.1. Unit Testing **Do This:** * Write unit tests for all critical functions and modules. * Use a testing framework like "unittest2". * Follow the Arrange-Act-Assert pattern. * Strive for high test coverage. **Why:** Unit tests verify the correctness of individual components and prevent regressions. **Example:** """nim import unittest2 import src/mymodule suite "MyModule Tests": test "myFunction returns the correct result": check myFunction(5) == 10 test "Can create MyType": let myInstance = MyType(field1: 1, field2: "test") check myInstance.field1 == 1 check myInstance.field2 == "test" runAllSuites() """ **Don't Do This:** * Skip writing unit tests. * Write tests that are too complex or tightly coupled to implementation details. * Ignore failing tests. ### 4.2. Integration Testing **Do This:** * Write integration tests to verify the interaction between different components. * Test the application's integration with external services and databases. * Use mocks or stubs to isolate components during testing. **Why:** Integration tests ensure that different parts of the application work together correctly. ### 4.3. End-to-End Testing **Do This:** * Write end-to-end tests to verify the entire application workflow from the user's perspective. * Use a testing framework like "karax" or "selenium" to automate browser interactions. * Run end-to-end tests in a dedicated testing environment. **Why:** End-to-end tests ensure that the application meets the overall requirements and provides a satisfactory user experience. ## 5. Documentation ### 5.1. API Documentation **Do This:** * Use doc comments ("##") to document all public types, procedures, and variables. * Include a brief description of the purpose, parameters, and return values. * Use the "#[...]" pragma to add metadata about the code. * Generate API documentation using "nim doc". **Why:** API documentation makes it easier for other developers to use and understand the code. **Example:** """nim ## Represents a user account. type User* = object ## The user's unique identifier. id*: int ## The user's username. username*: string ## The user's email address. email*: string ## Retrieves a user by their ID. ## ## Parameters: ## userId: The ID of the user to retrieve. ## ## Returns: ## The user with the given ID, or "nil" if no such user exists. proc getUserById*(userId: int): User {.raises: [IOError].} = # Implementation here discard """ **Don't Do This:** * Omit documentation for public API elements. * Write vague or incomplete documentation. * Fail to keep documentation up-to-date as the code changes. ### 5.2. Code Comments **Do This:** * Use comments to explain complex or non-obvious code. * Focus on explaining the *why* rather than the *what*. * Keep comments concise and relevant. **Why:** Code comments make it easier for other developers to understand the code's logic and intent. **Don't Do This:** * Comment on every line of code. * Write comments that are redundant or obvious. * Let comments become stale or inaccurate. ### 5.3 Usage Examples **Do This:** * Include basic, runnable usage examples. * Focus on the 'happy path' to show how to use code. * Keep examples short. **Why:** Usage examples will allow quick start to developers, and demonstrate expected use of the code. **Example** """nim # Example usage: import mymodule let result = myFunction(5) echo "Result: ", result # Output: Result: 10 """ This detailed document provides comprehensive development standards for Nim, considering modern approaches and best practices. By adhering to these guidelines, your Nim development team will be well-equipped to build robust, maintainable, and scalable applications.
# Performance Optimization Standards for Nim This document outlines coding standards and best practices specifically designed for performance optimization in Nim. Adhering to these standards will result in faster, more responsive, and resource-efficient applications. These standards apply to the latest version of Nim and emphasize modern approaches. ## 1. Algorithmic Efficiency and Data Structures ### 1.1. Algorithm Selection * **Do This:** Choose algorithms with optimal time complexity for the task at hand. Understand the trade-offs between different algorithms (e.g., sorting algorithms, search algorithms). * **Don't Do This:** Blindly use the first algorithm that comes to mind. Ignore the impact of algorithm choice on performance, especially with large datasets. **Why:** Choosing the right algorithm can dramatically impact performance. For example, using a bubble sort (O(n^2)) instead of a merge sort (O(n log n)) on a large dataset will lead to significantly slower execution. **Example:** """nim import algorithm # Good: Using timSort (hybrid merge sort/insertion sort) in the standard library let data = [3, 1, 4, 1, 5, 9, 2, 6] var sortedData = data sortedData.sort() # Uses timSort # Bad: Implementing a naive (and slow) bubble sort proc bubbleSort(data: var seq[int]) = let n = data.len for i in 0..<n: for j in 0..<n - i - 1: if data[j] > data[j+1]: swap data[j], data[j+1] var sortedDataBubble = data bubbleSort(sortedDataBubble) # Extremely slow for large datasets """ ### 1.2. Data Structure Optimization * **Do This:** Select the most appropriate data structure for the task. Consider factors like access patterns, insertion/deletion frequency, and memory usage. Leverage Nim's rich standard library of data structures. * **Don't Do This:** Use a data structure that doesn't fit the problem's requirements, leading to unnecessary overhead and poor performance. Overuse generic "seq" for everything. * **Why:** Using the wrong data structure can cause significant performance bottlenecks. For example, using a list for frequent random access is inefficient compared to using an array. **Example:** """nim import tables, sets # Good: Using a Table for key-value lookups with potential for string keys var nameToAge = initTable[string, int]() nameToAge["Alice"] = 30 nameToAge["Bob"] = 25 # Good: Using a Set to efficiently check for membership var seenNumbers = initSet[int]() seenNumbers.incl(10) if 10 in seenNumbers: echo "10 has been seen" # Bad: Using a seq to check if keys exist or perform fast lookups. var tempSeq: seq[(string, int)] # imagine filling tempSeq, but using "for (key, value) in tempSeq: if key == "Alice": ..." is extremely slow """ ### 1.3 Memory Management * **Do This:** Understand Nim's memory management options (GC, ARC, ORC) and choose the most suitable one for your application. Consider using "{.push gcsafe.}" and "{.pop.}" pragmas for performance-critical sections to prevent GC pauses. Prefer value types (copy semantics) where applicable to minimize heap allocations. Use object variants and enums. * **Don't Do This:** Rely solely on the default GC without considering its impact on latency. Create excessive temporary objects, increasing GC pressure. * **Why:** Nim's memory management system has a direct impact on performance, especially in long-running or real-time applications. Minimizing memory allocations and deallocations reduces GC overhead. The new ORC GC is designed for efficiency, especially with destructors and ownership. * **ORC GC (Latest Nim):** The ORC (Ownership and Reference Counting) GC in newer Nim versions (>= 2.0) manages memory using a combination of ownership tracking and reference counting. It's designed to minimize GC pauses and improve deterministic memory management. **Example:** """nim # Good: Using ARC/ORC for deterministic memory management # Nim compiler options: -d:arc or -d:orc proc processData(data: seq[int]): seq[int] = # Minimize allocations within the loop var result: seq[int] for item in data: result.add(item * 2) return result # Good: using GC Safe sections proc someCalculation(data: seq[int]) : seq[int] {.gcsafe.} = # no new memory allocations here, everything is on the stack. return data # Bad: Excessive allocation in a tight loop (causes frequent GC pauses) proc processDataBad(data: seq[int]): seq[int] = var result: seq[int] for item in data: result.add(new int(item * 2)) # Creates a new heap-allocated int for each item. """ ### 1.4 String Handling * **Do This:** Use "static[string]" for string literals that are known at compile time. Use "cstring" for interacting with C libraries. Pre-allocate strings of known size with "newString(size)". Use the "strutils" module efficiently. * **Don't Do This:** Perform unnecessary string copies in loops. Use string concatenation ("&") excessively within performance-critical loops, as each concatenation creates a new string. Underestimate the impact of string operations on performance. * **Why:** Strings are immutable in Nim, so concatenating or modifying them creates new string objects, which can be expensive. CStrings bypass the Nim memory manager entirely. **Example:** """nim import strutils # Good: Using StringBuilder for efficient string concatenation within a loop proc buildString(count: int): string = var sb = newStringOfCap(count * 2) for i in 0..<count: sb.add($i) # Efficiently appends to the string builder return sb # Good: Using static[string] const greeting: static[string] = "Hello, world!" # Bad: Inefficient string concatenation in a loop proc buildStringBad(count: int): string = var result = "" for i in 0..<count: result = result & $i # Inefficient: creates a new string in each iteration return result """ ## 2. Concurrency and Parallelism ### 2.1. Threading and Async * **Do This:** Utilize Nim's threading and async features to parallelize CPU-bound and I/O-bound tasks. Use channels for communication between threads. Use "spawn" for lightweight concurrency with the "async" library. Use "parallel" iterators for embarassingly parallel computations. * **Don't Do This:** Overuse threads (thread creation is relatively expensive). Neglect proper synchronization, leading to race conditions and data corruption. Block the main thread with long-running operations. * **Why:** Concurrency and parallelism can dramatically improve performance by distributing work across multiple cores or overlapping I/O operations with computation. **Example (Threading):** """nim import threadpool, locks var result: array[10, int] lock: Lock proc worker(i: int) = lock.acquire() try: result[i] = i * i finally: lock.release() proc main()= var pool = newThreadPool() lock.initLock() for i in 0 ..< result.len: pool.do(worker, i) pool.join() # Wait for all tasks to complete echo result main() """ **Example (Async):** """nim import asynchttpserver proc requestHandler(request: Request): Future[void] {.async.} = # Simulate an I/O-bound operation await sleepAsync(1000) # Simulate waiting for I/O request.respond(Http200, "Hello, async world!") proc main()= asyncCheck server.serve(Port(8080), requestHandler) waitFor(server.run()) # Run server, waiting for completion. In prod: gracefulShutdown() main() """ ### 2.2. Data Sharing and Synchronization * **Do This:** Use locks, channels, and atomic operations to protect shared data from concurrent access. Consider using immutable data structures to avoid the need for synchronization. Think about message passing semantics versus shared memory. * **Don't Do This:** Assume that data is thread-safe without proper synchronization. Create deadlocks by acquiring locks in inconsistent orders. * **Why:** Concurrent access to shared data without proper synchronization can lead to race conditions and data corruption. **Example:** """nim import locks, atomics var counter: Atomic[int] lock: Lock proc incrementCounter() = lock.acquire() try: counter.inc() # Atomic increment finally: lock.release() # Another approach using the "atomics" library directly proc incrementCounterAtomic() = while true: let oldValue = counter.get() let newValue = oldValue + 1 if counter.compareAndSet(oldValue, newValue): break proc main() = lock.initLock() # Simulate multiple threads incrementing the counter var threads: array[5, Thread[void]] for i in 0..<threads.len: createThread(threads[i], incrementCounter) #incrementCounterAtomic) for thread in threads: thread.join() echo "Counter value: ", counter.get() main() """ ## 3. Code-Level Optimizations ### 3.1. Inlining * **Do This:** Use the "{.inline.}" pragma for small, frequently called procedures. Allow the compiler to inline functions automatically based on heuristics (link-time optimization). * **Don't Do This:** Inline large or complex procedures, as this can increase code size and potentially degrade performance. * **Why:** Inlining avoids the overhead of a function call by replacing the call site with the function's code directly. **Example:** """nim # Good: Inlining a small, frequently called procedure proc square(x: int): int {.inline.} = return x * x proc calculateArea(side: int): int = return square(side) # The compiler will likely inline the square procedure # Bad: Inlining a large function could bloat the code. """ ### 3.2. Loop Optimization * **Do This:** Minimize computations inside loops. Use loop unrolling techniques where appropriate. Keep loop bodies small for better cache locality. Use "static" variables inside loops if the value does not change per iteration. * **Don't Do This:** Perform redundant calculations within loops. Access memory in a non-sequential manner (e.g., jumping around). * **Why:** Loops are often performance hotspots, so optimizing them can significantly improve overall performance. **Example:** """nim # Good: Hoisting calculations outside the loop proc processData(data: seq[int], factor: int): seq[int] = let scaledFactor = factor * 2 # Calculate outside the loop var result: seq[int] = @[] for item in data: result.add(item * scaledFactor) return result # Good: Using a static variable when possible proc processDataStatic(data: seq[int]) : seq[int] = var result: seq[int] = @[] for item in data: static let someValue = 10 # Only computed once. result.add(item * someValue) return result # Bad: Redundant calculation inside the loop proc processDataBad(data: seq[int]): seq[int] = var result: seq[int] = @[] for item in data: result.add(item * 2 * 2) # Redundant: 2*2 is calculated in each iteration return result """ ### 3.3. Type Specialization * **Do This:** Use generics and "typedesc" parameters to create specialized versions of procedures for different data types. This allows the compiler to optimize code for specific types, improving performance. This is especially helpful for numeric code. * **Don't Do This:** Write generic code that doesn't take advantage of type specialization opportunities. * **Why:** Type specialization allows the compiler to generate more efficient code by eliminating runtime type checks and utilizing type-specific optimizations. **Example:** """nim # Good: Using a template for type specialization template add(a, b: untyped): untyped = a + b proc addInt(a, b: int): int = add(a, b) proc addFloat(a, b: float): float = add(a, b) # This generates specialized add procedures for int and float. """ ### 3.4. Conditional Compilation * **Do This:** Use conditional compilation ("{.defined(flag).}") to enable or disable code sections based on target architecture, operating system, or other build-time flags. This can be used to provide optimized code paths for specific platforms. * **Don't Do This:** Overuse conditional compilation, leading to complex and hard-to-maintain code. * **Why:** Conditional compilation allows you to tailor your code to specific environments, enabling platform-specific optimizations. **Example:** """nim # Good: Using conditional compilation for platform-specific optimizations when defined(windows): proc doWindowsSpecificThing() = echo "Doing Windows-specific thing" elif defined(linux): proc doLinuxSpecificThing() = echo "Doing Linux-specific thing" else: proc doGenericThing() = echo "Doing generic thing" doWindowsSpecificThing() # Calls either windos, linux, or generic version at compile time. """ ## 4. Compiler Optimizations ### 4.1. Optimization Flags * **Do This:** Use the "-d:release" flag for production builds to enable compiler optimizations. Experiment with different optimization levels ("-opt:speed", "-opt:size") to find the best trade-off between performance and code size. Use "-lto:on" for link time optimization. * **Don't Do This:** Deploy code without enabling compiler optimizations. * **Why:** Compiler optimizations can significantly improve performance by automatically applying various code transformations. **Example:** """bash nim c -d:release --opt:speed --lto:on my_program.nim """ ### 4.2. Profile-Guided Optimization (PGO) * **Do This:** Consider using PGO to further optimize your code based on runtime profiling data. This can be a multi-step build process, where first a profiled version is run, then used to generate optimized code. * **Don't Do This:** Neglect PGO for performance-critical applications. * **Why:** PGO allows the compiler to make more informed optimization decisions based on actual usage patterns. **Example:** (requires extra build steps, consult Nim documentation) - Example for future versions """bash # 1. Compile with profiling enabled nim c -d:release --opt:speed --pgo:on my_program.nim # 2. Run the program with a representative workload to generate profiling data ./my_program # 3. Recompile using the profiling data nim c -d:release --opt:speed --pgo:use my_program.nim """ ## 5. Profiling and Benchmarking ### 5.1. Performance Profiling * **Do This:** Use profiling tools to identify performance bottlenecks in your code. Instruments like "perf" on Linux or Instruments on macOS can show CPU usage, memory allocation, and other performance metrics. Nim also has its own profiler. * **Don't Do This:** Guess where the performance bottlenecks are. Optimize code without measuring its impact. * **Why:** Profiling provides valuable insights into where your code is spending the most time, allowing you to focus your optimization efforts effectively. **Example:** (using Nim's profiler -- needs nim >= 2.0) """nim import profile proc slowFunction() = var x = 0 for i in 0..1000000: x += i proc main() = startProfile() slowFunction() stopProfile() saveProfile("profile.data") # then use "nim profiler profile.data" main() """ ### 5.2. Benchmarking * **Do This:** Use benchmarking tools to measure the performance of your code changes. Use the "benchmarks" module in Nim to create reproducible benchmarks. * **Don't Do This:** Rely on anecdotal evidence to assess performance improvements. * **Why:** Benchmarking provides quantitative data to verify the effectiveness of your optimizations changes, and catches regressions. **Example:** """nim import benchmarks benchmark "String concatenation": var s = "" for i in 0..<1000: s &= "x" benchmark "StringBuilder": var sb = newStringOfCap(1000) for i in 0..<1000: sb.add("x") discard sb.toString runBenchmarks() """ ## 6. System-Level Considerations ### 6.1. I/O Optimization * **Do This:** Use buffered I/O to reduce the number of system calls. Minimize disk access by caching frequently used data in memory. Use asynchronous I/O where appropriate. * **Don't Do This:** Perform unbuffered I/O for large data transfers. Make excessive disk accesses. * **Why:** I/O operations are often a performance bottleneck, so optimizing them can significantly improve application performance. **Example:** """nim # Good: Using buffered I/O import streams proc readFileBuffered(filename: string): string = let fileStream = newFileStream(filename, fmRead) defer: fileStream.close() # Wrap the file stream with a buffered stream let bufferedStream = newBufferedStream(fileStream) return bufferedStream.readAll() # Efficiently reads the entire file # Bad: Reading a file character by character without buffering proc readFileUnbuffered(filename: string): string = let fileStream = newFileStream(filename, fmRead) defer: fileStream.close() var result = "" while not fileStream.atEnd(): result.add(fileStream.readChar()) # Inefficient: makes a system call for each character return result """ ### 6.2. Memory Usage * **Do This:** Monitor your application's memory usage to identify potential memory leaks or excessive memory consumption. Use tools like Valgrind (Linux) to detect memory errors. Consider using data compression techniques to reduce memory footprint. * **Don't Do This:** Ignore memory usage patterns. Allocate large amounts of memory without reason. * **Why:** High memory usage can lead to reduced performance, crashes, or even denial-of-service. Leaked memory might cause even bigger issues down the road. ## 7. Specific Nim Features and Libraries ### 7.1 Zero-Cost Iterators * **Do This:** When appropriate, write custom iterators rather than generating sequences with "map" or similar. These iterators perform the computation during the loop, rather than computing the complete sequence in-memory first. * **Don't Do This:** Always generate sequences, especially large ones, before looping, wasting memory and compute cycles. **Example** """nim # Good: using a custom iterator iterator numbers(n: int): int = for i in 0..<n: yield i for num in numbers(10): echo num # Bad: materializing the seq before processing it. proc numbersSeq(n: int) : seq[int] = result = newSeq[int](n) for i in 0..<n: result[i] = i for num in numbersSeq(10): echo num # less efficient, as numbersSeq is allocating "n" integers. """ ### 7.2 Unchecked Operations * **Do This:** For performance-critical numeric operations, carefully consider using "unchecked" operations (e.g., "inc(x, unchecked)"). **Only** use these when you are *absolutely certain* that overflow or other errors cannot occur, and you understand the implications for program correctness and security. Document *why* the operations are "unchecked". * **Don't Do This:** Use "unchecked" operations indiscriminately. **Example:** """nim var x = high(int) inc(x, unchecked) # Use unchecked only when overflow is managed and intentional. Example: wrapping 2D coordinates to 256 (tilesize) echo x # output '-2147483648'. Programmers guarantee this is the CORRECT result for the application. """ ### 7.3 Result Attribute * **Do This:** Use the "result" keyword when assigning the return value in procedures, especially for single-expression returns. This can improve readability and potentially enable further optimizations. * **Don't Do This:** Initialize a temporary result variable when not needed. **Example:** """nim proc double(x: int): int = result = x * 2 # result keyword implies this is the return proc anotherDouble(x: int) : int = var res = x * 2 return res # Less succinct. "result" is better (generally.) """ These standards aim to help you write performant, readable, and maintainable Nim code, taking full advantage of the language's capabilities and the surrounding ecosystem. Adapt and refine these guidelines based on the specific needs and constraints of each project.