# Security Best Practices Standards for Nim
This document outlines security best practices for Nim development. It's designed to guide developers in writing secure, maintainable, and performant Nim code, particularly when integrating with other systems or handling external input. Keeping security in mind from the start of a project dramatically reduces both risks and long-term costs.
## 1. Input Validation and Sanitization
### 1.1. Standard: Validate all external input.
* **Do This:** Validate all data received from external sources, including user input, network requests, file reads, and command-line arguments.
* **Don't Do This:** Assume external data is safe or in the expected format.
* **Why:** Failure to validate input can lead to numerous vulnerabilities, including injection attacks, buffer overflows, and denial-of-service attacks.
"""nim
import strutils, os
proc processUsername(username: string) =
# Only allow alphanumeric characters and underscores.
if username.len > 32:
raise newException(ValueError, "Username too long")
if not username.allCharsInSet({'a'..'z', 'A'..'Z', '0'..'9', '_'}):
raise newException(ValueError, "Invalid characters in username")
echo "Processing username: " & username
# Example usage (command-line argument)
if paramCount() > 0:
try:
let username = paramStr(1)
processUsername(username)
except ValueError as e:
echo "Error: " & e.msg
else:
echo "Please provide a username as a command-line argument."
"""
**Anti-pattern:**
"""nim
# Bad practice: No input validation.
proc processUsername(username: string) =
# This code is VOLUNERABLE to injection attacks or unexpected behavior
# if the username contains special characters or is excessively long.
echo "Processing username: " & username
"""
### 1.2 Standard: Use allow lists instead of block lists whenever possible.
* **Do This:** Define a set of allowed values or patterns and reject anything that doesn't conform.
* **Don't Do This:** Try to anticipate and block all possible malicious inputs. Blocklists are incomplete and quickly out of date.
* **Why:** Allow lists provide a more positive and secure approach, ensuring that only expected and safe input is processed.
"""nim
import strutils
type
Color = enum
Red, Green, Blue
proc parseColor(colorStr: string): Color =
case colorStr.toLowerAscii:
of "red":
Red
of "green":
Green
of "blue":
Blue
else:
raise newException(ValueError, "Invalid color: " & colorStr)
# Example usage
try:
let color = parseColor("Red")
echo "Color: " & color
except ValueError as e:
echo "Error: " & e.msg
"""
### 1.3 Standard: Sanitize input to remove potentially harmful characters or sequences.
* **Do This:** Escape or remove characters that could be interpreted as code or commands.
* **Don't Do This:** Pass untrusted data directly to system calls or other potentially dangerous functions.
* **Why:** Sanitization prevents injection attacks and ensures that input is treated as data, not executable code.
"""nim
import strutils, htmlparser
proc sanitizeHTML(html: string): string =
# Note: This example uses a 3rd party library.
# Sanitize using HTML escaping
escapeHTML(html)
# Example usage
let untrustedHTML = "Hello, world!"
let sanitizedHTML = sanitizeHTML(untrustedHTML)
echo "Sanitized HTML: " & sanitizedHTML
"""
## 2. Preventing Injection Attacks
### 2.1 Standard: Use parameterized queries or prepared statements for database interactions.
* **Do This:** Pass user-provided data as parameters to database queries rather than embedding it directly in the SQL string.
* **Don't Do This:** Concatenate strings to build SQL queries.
* **Why:** Parameterized queries prevent SQL injection attacks by ensuring that user input is treated as data and not as part of the SQL command.
"""nim
import db_sqlite
proc createUser(db: DbConn, username, passwordHash: string) =
let sql = "INSERT INTO users (username, password_hash) VALUES (?, ?)"
db.exec(sql, @[username, passwordHash])
# Example of using this:
# Assuming you have a passwordHash function implemented elsewhere that returns
# a safe hash of the user-provided password.)
# let passwordHashValue = passwordHash(userProvidedPassword)
# createUser(myDatabaseConnection, userProvidedUsername, passwordHashValue)
"""
### 2.2 Standard: Use the "execCmdEx" function with proper escaping for system calls.
* **Do This:** Use "execCmdEx" with the "options = {poShell}" option and provide arguments as a list.
* **Don't Do This:** Use "execCmd" or string interpolation directly, which is vulnerable to command injection.
* **Why:** "execCmdEx" with proper escaping sanitizes arguments, preventing attackers from injecting malicious commands.
"""nim
import os
proc createDirectory(dirname: string) =
# Properly escape the directory name.
let result = execCmdEx("mkdir", @[dirname], options = {poShell})
if result.exitCode != 0:
raise newException(OSError, "Failed to create directory: " & result.output)
# Example usage
try:
createDirectory("safe_directory_name")
createDirectory("directory with spaces") #Spaces handled correctly by poShell
# NEVER do createDirectory(paramStr(1)) without prior validation - it is unsafe
except OSError as e:
echo "Error: " & e.msg
"""
**Anti-pattern:**
"""nim
import os
proc createDirectoryUnsafe(dirname: string) =
# Vulnerable to command injection if dirname contains shell metacharacters.
let result = execCmd("mkdir " & dirname) # VERY DANGEROUS
if result != 0:
raise newException(OSError, "Failed to create directory")
"""
### 2.3 Standard: When interacting with external services (e.g., APIs), validate responses and handle errors gracefully.
* **Do This:** Check the status codes and data integrity of responses from external services. Implement robust error handling to prevent crashes or the exposure of sensitive information.
* **Don't Do This:** Assume external services are always available or that their responses are always valid.
* **Why:** External services can be unreliable or malicious. Validating responses prevents your application from being compromised by faulty or malicious data.
"""nim
import httpclient, json
proc fetchUserData(userId: int): JsonNode =
let client = newHttpClient()
let url = "https://api.example.com/users/" & $userId
try:
let response = client.get(url)
if response.status == Http200:
return parseJson(response.body)
else:
raise newException(OSError, "API request failed with status code: " & $response.status)
except Exception as e:
raise newException(OSError, "API request failed: " & e.msg)
# Example usage
try:
let userData = fetchUserData(123)
echo "User data: " & userData.repr
except OSError as e:
echo "Error: " & e.msg
"""
## 3. Authentication and Authorization
### 3.1 Standard: Use strong password hashing algorithms.
* **Do This:** Use Argon2, bcrypt, or scrypt for password hashing. These algorithms are designed to be resistant to brute-force attacks.
* **Don't Do This:** Use MD5 or SHA1 for password hashing. These algorithms are considered insecure and easily cracked. Do NOT store passwords in plain text.
* **Why:** Strong password hashing protects user credentials from being compromised in the event of a data breach.
"""nim
import chronicle
#NOTE: Chronicle is considered a high quality hashing library for Nim
proc hashPassword(password: string): string =
let (ok, hash) = Argon2.hash(password) #default is argon2id
if not ok:
raise newException(ValueError, "Argon2 hashing failed")
return hash
proc verifyPassword(password, hashedPassword: string): bool =
Argon2.verify(hashedPassword,password)
# Example usage:
let password = "mySecretPassword"
let hashedPassword = hashPassword(password)
echo "Hashed password: " & hashedPassword
let isCorrect = verifyPassword(password, hashedPassword)
echo "Password verification: " & isCorrect
"""
### 3.2 Standard: Implement robust rate limiting and account lockout mechanisms.
* **Do This:** Limit the number of login attempts within a specific time frame. Lock accounts after a certain number of failed attempts.
* **Don't Do This:** Allow unlimited login attempts or fail to implement account lockout.
* **Why:** Rate limiting and account lockout prevent brute-force attacks and protect user accounts from unauthorized access.
*Implementing rate limiting fully depends on the framework chosen and storage (e.g. Redis). The below shows a basic pattern for storing an access record.
"""nim
import times, strutils
type
AccessRecord = object
timestamp: DateTime
ipAddress: string
var accessLog: seq[AccessRecord] # Ideally, put in Redis, DB, etc.
const
MaxAttempts = 5
LockoutDuration = 60 * 60 # seconds ie 1hr
proc recordAttempt(ipAddress: string) =
accessLog.add(AccessRecord(timestamp: now(), ipAddress: ipAddress))
proc attemptsWithinLockout(ipAddress: string): int =
let lockoutStart = now() - seconds(LockoutDuration)
var count = 0
for record in accessLog:
if record.ipAddress == ipAddress and record.timestamp >= lockoutStart:
inc(count)
return count
proc isLockedOut(ipAddress: string): bool =
return attemptsWithinLockout(ipAddress) >= MaxAttempts
proc login(username, password, ipAddress: string): bool =
if isLockedOut(ipAddress):
echo "Account locked out for IP address: " & ipAddress
return false
# ... Your authentication logic ...
# If login fails:
recordAttempt(ipAddress)
return false # Or true on success
"""
### 3.3 Standard: Use established authorization patterns (Role-Based Access Control (RBAC), Attribute-Based Access Control (ABAC)).
* **Do This:** Define roles and permissions for different user groups. Control access to resources based on roles or attributes.
* **Don't Do This:** Implement ad-hoc authorization logic that is difficult to maintain and audit.
* **Why:** RBAC and ABAC provide a structured way to manage access control, ensuring that users only have access to the resources they need.
"""nim
type
Role = enum
Admin,
Editor,
Viewer
User = object
username: string
roles: set[Role]
proc hasPermission(user: User, requiredRole: Role): bool =
return requiredRole in user.roles
# Example usage
let
adminUser = User(username: "admin", roles: {Admin, Editor, Viewer})
editorUser = User(username: "editor", roles: {Editor, Viewer})
viewerUser = User(username: "viewer", roles: {Viewer})
if hasPermission(adminUser, Admin):
echo "Admin user has admin privileges."
if hasPermission(editorUser, Admin):
echo "Editor user has admin privileges." # Will *not* be called
"""
## 4. Data Protection and Encryption
### 4.1 Standard: Encrypt sensitive data at rest and in transit.
* **Do This:** Use encryption algorithms like AES for data at rest. Use HTTPS (TLS) for data in transit.
* **Don't Do This:** Store sensitive data in plain text or transmit it over unencrypted channels.
* **Why:** Encryption protects sensitive data from unauthorized access, even if the system is compromised.
"""nim
import std/os
import std/strutils
import std/random
# Use a trusted Cryptographic library such as "Locks"
# Locks provides an easy, straightforward API, and has seen significant use.
import locks/aes
proc generateKey(bits: int): string =
# Generate a random key of the specified number of bits.
let bytesNeeded = bits div 8
result = cryptRand(bytesNeeded).toHex
proc encryptData(data: string, key: string): string =
# Encrypt the data using AES.
return Aes.encrypt(data, key)
proc decryptData(encryptedData: string, key: string): string =
# Decrypt the data using AES.
return Aes.decrypt(encryptedData, key)
# Example usage
let key = generateKey(256) # Generate a 256-bit key(32 bytes)
let sensitiveData = "My Secret Data"
let encryptedData = encryptData(sensitiveData, key)
echo "Encrypted data: " & encryptedData
let decryptedData = decryptData(encryptedData, key)
echo "Decrypted data: " & decryptedData
"""
### 4.2 Standard: Use secure random number generators for cryptographic operations.
* **Do This:** Use "std/random.cryptRand" or "std/os.random" for generating keys, IVs, and other cryptographic parameters.
* **Don't Do This:** Use "rand" or other pseudo-random number generators for cryptographic purposes.
* **Why:** Secure random number generators produce unpredictable values, preventing attackers from predicting or manipulating cryptographic operations.
See "generateKey" in the above example.
### 4.3 Standard: Store cryptographic keys securely.
* **Do This:** Use hardware security modules (HSMs), key management systems (KMS), or secure configuration files with restricted access to store cryptographic keys.
* **Don't Do This:** Hardcode keys in the source code or store them in plain text in configuration files.
* **Why:** Protecting cryptographic keys is essential for maintaining the confidentiality and integrity of encrypted data.
*Key management and storage is a large topic. Modern solutions such as Hashicorp Vault are worth investigating.*
## 5. Error Handling and Logging
### 5.1 Standard: Implement comprehensive error handling and logging.
* **Do This:** Catch exceptions, log errors, and provide informative error messages to users.
* **Don't Do This:** Ignore exceptions, suppress error messages, or expose sensitive information in error logs.
* **Why:** Proper error handling and logging enable you to identify and address security vulnerabilities, monitor system health, and investigate security incidents.
"""nim
import logging
# Logger setup
var logger = newFileLogger(filename = "app.log", level = lvlInfo)
proc processData(data: string) =
try:
# Simulate a potential error.
if data == "error":
raise newException(ValueError, "Invalid data")
echo "Processing data: " & data
except ValueError as e:
logger.error("Error processing data: " & e.msg) #NOTE this will also log the exception trace!
echo "An error occurred. Please check the logs." # User-friendly message
except Exception as e: #Catch anything else
logger.error("Unexpected error: " & e.msg)
echo "An unexpected error occurred. Please contact support."
# Example usage
processData("valid data")
processData("error") # Will trigger an error, logging to file
"""
### 5.2 Standard: Sanitize or mask sensitive information in logs.
* **Do This:** Remove or replace sensitive data (e.g., passwords, API keys, credit card numbers) with placeholders or hashes before logging.
* **Don't Do This:** Log sensitive data in plain text.
* **Why:** Preventing sensitive data from being exposed in logs protects it from unauthorized access in case of a security breach.
"""nim
import logging, strutils
# Logger setup
var logger = newFileLogger(filename = "app.log", level = lvlInfo)
proc logSensitiveData(username: string, apiKey: string) =
# Mask the API key before logging.
let maskedApiKey = apiKey.replace(apiKey[4..^4], "****")
logger.info("User " & username & " used API key " & maskedApiKey)
# Example usage
let username = "johndoe"
let apiKey = "ABCDEFGHIJKLMNOPQRSTUVWXYZ123456"
logSensitiveData(username, apiKey)
"""
### 5.3 Standard: Implement intrusion detection and prevention systems (IDS/IPS).
* **Do This:** Use tools to monitor your system for suspicious activity and automatically block or mitigate attacks.
* **Don't Do This:** Rely solely on manual monitoring or fail to take proactive measures to protect your system.
* **Why:** An Intrusion Detection/Prevention System helps identify and neutralise threats.
*Nim implementations of IDS/IPS are highly project dependant, but examples in other languages can be ported.*
## 6. Dependency Management
### 6.1 Standard: Use a dependency management tool (Nimble) to manage third-party libraries.
* **Do This:** Declare all dependencies in your Nimble package file (".nimble").
* **Don't Do This:** Manually download and add libraries to your project, as it can introduce security risks.
* **Why:** Dependency management tools ensure that you are using the correct versions of libraries and can help identify and resolve security vulnerabilities.
**Example ".nimble" file:**
"""nim
version = "0.1.0"
author = "Your Name"
description = "My secure Nim application"
license = "MIT"
requires "nim >= 1.6.0"
dependencies.chronicle = { github = "def-/chronicle" }
dependencies.db_sqlite = { github = "nitely/nim-sqlite" }
"""
### 6.2 Standard: Regularly update dependencies to the latest versions.
* **Do This:** Keep your dependencies up-to-date with security patches and bug fixes.
* **Don't Do This:** Use outdated dependencies that may contain known vulnerabilities.
* **Why:** Updating dependencies reduces the risk of exploiting known vulnerabilities in third-party libraries.
"nimble update"
### 6.3 Standard: Verify the integrity of downloaded dependencies.
* **Do This:** Use checksums or digital signatures to verify that downloaded dependencies have not been tampered with.
* **Don't Do This:** Trust downloaded dependencies without verifying their integrity.
* **Why:** Verifying integrity prevents attackers from injecting malicious code into your application through compromised dependencies.
*Nimble doesn't yet have native checksum verification. Consult the library's own website or repo for checksum or signature verification guides, if offered*
## 7. Secure Configuration Management
### 7.1 Standard: Store configuration data securely.
* **Do This:** Use environment variables, encrypted configuration files, or dedicated secret management tools to store sensitive configuration data.
* **Don't Do This:** Hardcode secrets, passwords, and API keys in your source code or store them in plaintext configuration files.
* **Why:** Secure configuration management protects sensitive data from unauthorized access and prevents attackers from gaining control of your application.
"""nim
import os
proc getApiKey(): string =
# Get the API key from an environment variable.
let apiKey = getEnv("API_KEY")
if apiKey.len == 0:
raise newException(OSError, "API_KEY environment variable not set")
return apiKey
# Example usage
try:
let apiKey = getApiKey()
echo "API key: " & apiKey
except OSError as e:
echo "Error: " & e.msg
"""
### 7.2 Standard: Restrict access to configuration files.
* **Do This:** Set appropriate file permissions to prevent unauthorized users from reading or modifying configuration files.
* **Don't Do This:** Leave configuration files world-readable or writable.
* **Why:** Restricting access to configuration files prevents attackers from modifying application settings or obtaining sensitive information.
### 7.3 Standard: Regularly rotate cryptographic keys and secrets.
* **Do This:** Periodically generate new cryptographic keys and secrets and invalidate the old ones.
* **Don't Do This:** Use the same keys and secrets indefinitely.
* **Why:** Key rotation limits the impact of a key compromise and reduces the risk of attackers using stolen keys to access sensitive data.
## 8. Code Review
### 8.1 Standard: Conduct regular code reviews with a focus on security.
* **Do This:** Review code for common security vulnerabilities, such as injection attacks, buffer overflows, and authentication flaws.
* **Don't Do This:** Skip code reviews or conduct them without a security focus.
* **Why:** Code reviews help identify and address security vulnerabilities early in the development process, reducing the risk of security incidents.
### 8.2 Standard: Use static analysis tools to identify potential security flaws.
* **Do This:** Integrate static analysis tools into your development workflow to automatically detect common security vulnerabilities.
* **Don't Do This:** Rely solely on manual code reviews or fail to use static analysis tools.
* **Why:** Static analysis tools can help identify security vulnerabilities that may be missed by human reviewers. This includes running "nim check" with the highest warning level.
### 8.3 Standard: Conduct penetration testing and security audits.
* **Do This:** Periodically engage external security experts to conduct penetration testing and security audits to identify and address security vulnerabilities.
* **Don't Do This:** Assume your application is secure without conducting regular security testing.
* **Why:** Penetration testing and security audits can help identify vulnerabilities that may not be apparent through code reviews or static analysis.
## 9. Denial of Service (DoS) Prevention
### 9.1 Standard: Implement resource limits.
* **Do This:** Limit the amount of memory, CPU, and disk space that each user or process can consume.
* **Don't Do This:** Allow unlimited resource consumption, which can lead to resource exhaustion and denial of service.
* **Why:** Resource limits prevent attackers from consuming excessive resources and causing the system to become unavailable.
### 9.2 Standard: Implement rate limiting.
* **Do This:** Limit the number of requests that each user or IP address can make within a specific time frame.
* **Don't Do This:** Allow unlimited requests, which can lead to request flooding and denial of service.
* **Why:** Rate limiting prevents attackers from overwhelming the system with excessive requests.
Refer to 3.2
### 9.3 Standard: Use appropriate data structures and algorithms to avoid algorithmic complexity attacks.
* **Do This:** Choose data structures and algorithms that have a predictable and efficient performance profile.
* **Don't Do This:** Use data structures or algorithms that can be easily exploited to cause excessive CPU usage or memory consumption.
* **Why:** Algorithmic complexity attacks can cause a denial of service by exploiting inefficient algorithms to consume excessive resources.
## 10. Further Recommendations
* **Stay informed:** Keep up to date with the latest security threats and vulnerabilities. Subscribe to security mailing lists, follow security experts on social media, and attend security conferences.
* **Follow the principle of least privilege:** Grant users and processes only the minimum necessary privileges to perform their tasks.
* **Implement defense in depth:** Use multiple layers of security controls to protect against a variety of threats.
* **Test your security controls:** Regularly test your security controls to ensure that they are effective.
* **Have an incident response plan:** Develop a plan for responding to security incidents, including procedures for identifying, containing, and recovering from attacks.
By following these security best practices, you can significantly reduce the risk of security vulnerabilities in your Nim applications and protect your data and systems from attack. Remember to stay informed about the latest security threats and best practices and to continuously improve your security posture.
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.