-
Notifications
You must be signed in to change notification settings - Fork 24
Coding style
Guiding principles and coding conventions for the Hydra project. These apply to all Hydra implementations and to contributions from both humans and AI assistants.
Hydra is a strongly-typed language built on the composition of functions. It aims for mathematical precision, and does not tolerate or hide failures.
- If there is an error, the application should fail immediately with an informative error message. Whether a resource fails to load, a mapping encounters an unexpected condition, or a type check fails, that is the end of the workflow.
-
Hydra code should never be written to recover from errors, except in limited cases involving I/O.
Do not catch exceptions and return defaults. Do not use
Data.Maybe.fromJust,Maybes.fromMaybe defaultValue, or equivalent patterns to silently paper over failures. (Hydra removedhydra.lib.maybes.fromJustas a primitive in v0.15.0 for this reason; if you need to collapse aMaybein a way that expresses "this cannot be Nothing," returnEither Error aand propagate the error instead.) -
Error messages must be informative. Include context about what was being attempted and what went wrong.
In Haskell, use
fail $ "Context: " ++ formatError errin I/O code, and propagateEither Errorvalues in pure code (threading aContextalongside for trace messages). - No post-generation patches. Never write a script to "fix" generated code after the code generation process runs. If there is a flaw in code generation, investigate and fix it at the source.
-- CORRECT: Fail immediately with context
case CodeGeneration.generateSourceFiles ... of
Left ic -> fail $ "Failed to generate source files: " ++ formatError ic
Right files -> do
mapM_ writePair files
return $ length files
-- WRONG: Silently swallow errors
let result = fromMaybe defaultTerm (lookupElement name graph)
-- WRONG: Catch and continue
case lookupElement name graph of
Left _ -> return emptyTerm -- Error is hidden
Right e -> return eHydra is a translingual programming language. Even code which is specific to one host or target language should be written in such a way that it can be generalized to other languages in the future.
- Take advantage of language-specific features, but always write code with an eye toward portability.
- The Hydra kernel is code-generated into Haskell, Java, Python, Scala, TypeScript, and four Lisp dialects (Clojure, Scheme, Common Lisp, Emacs Lisp). Design choices in one implementation should not preclude the same logic working in another.
- Avoid idioms that are deeply tied to one language's runtime (e.g., Java-specific exception hierarchies, Python-specific dynamic dispatch patterns) when writing kernel-level code.
- When adding primitives, implement the same semantics across all six full implementations. See the adding primitives recipe.
Writing code in a standardized way improves maintainability and extensibility.
-
Definitions are always provided in alphabetical order so it is easy to find the function you are looking for. This applies to the list of definition names in module sources and to the definition definition bodies which follow.
-
Conventional names are reused across namespaces to decrease mental load. When similar operations exist in different modules, they use the same name. For example,
ns,define, andmodule_, appear in every source module with the same roles. Hydra Core terms liketerm,field,record, etc. appear in many different DSLs used for different purposes in the kernel. -
Type-indexed modules reinforce this consistency. Hydra's source code is organized into families of modules that mirror the type modules. For a given type module like
hydra.core(which definesAnnotatedTerm,AnnotatedType,Application, etc.), there are parallel modules:-
hydra.show.core-- with functionsannotatedTerm,annotatedType,application, etc. -
hydra.encode.core-- same function names, different operation -
hydra.decode.core-- same function names -
hydra.extract.core-- same function names -
hydra.error.core-- same function names -
hydra.path.core-- same function names -
hydra.dsl.core(generated DSL) -- same function names
Each family member provides a different operation (showing, encoding, decoding, extracting, etc.) on the same set of types, and the functions are always named after the type they operate on. This makes the codebase highly navigable: if you know the type name and the operation you need, you know the function name.
-
-
Import conventions are consistent across files. The same modules are qualified the same way everywhere (e.g.,
Lists,Strings,Core,Graph). See import conventions below.
Additionally, in the Hydra kernel:
-
Simple names only: kernel modules have names like
hydra.coreorhydra.show.packaging, never capitalized or camel-cased names likehydra.someModule. Test modules are not part of the kernel, and are not subject this rule; e.g.hydra.test.checking.algebraicTypes.
Every Hydra source module follows this layout:
- Module-level Haddock comment
- Module declaration
- Imports (see import conventions)
-
ns(namespace) definition -
definehelper (created withdefinitionInNamespace nsordefinitionInModule module_) -
module_definition withelementslist - Function definitions, in the same order as the
elementslist
-- | Description of what this module provides.
module Hydra.Sources.MyModule where
import Hydra.Kernel
import Hydra.Sources.Libraries
-- ... other imports ...
ns :: ModuleName
ns = ModuleName "hydra.myModule"
define :: String -> TTerm a -> TTermDefinition a
define = definitionInModuleName ns
module_ :: Module
module_ = Module {
moduleName = ns,
moduleDefinitions = definitions,
moduleDependencies = unqualifiedDep <$>
([dependencies] L.++ kernelTypesModuleNames),
moduleDescription = Just "Description of the module."}
where
definitions = [
toTermDefinition alpha,
toTermDefinition beta,
toTermDefinition gamma]
alpha :: TTermDefinition (...)
alpha = define "alpha" $ ...
beta :: TTermDefinition (...)
beta = define "beta" $ ...
gamma :: TTermDefinition (...)
gamma = define "gamma" $ ...Definitions in the elements list and their corresponding implementations must be in
alphabetical order. This makes it easy to locate any definition by name without searching.
-- CORRECT: Alphabetical
definitions = [
toTermDefinition compactName,
toTermDefinition localNameOf,
toTermDefinition moduleNameOf,
toTermDefinition qualifyName,
toTermDefinition unqualifyName]
-- WRONG: Arbitrary or "logical" ordering
definitions = [
toTermDefinition qualifyName,
toTermDefinition unqualifyName,
toTermDefinition compactName, -- Out of place
toTermDefinition moduleNameOf,
toTermDefinition localNameOf] -- Out of placeHydra Haskell DSL source files follow a consistent import structure across eight categories of source module. The general rule is: qualify by default, and unqualify only the small set of modules that are central to what the file builds.
For the canonical import block of each category, see Import conventions in the main repo. The eight categories are:
-
kernel type modules— Hydra-kernel types -
type-level sources outside of the kernel— Per-language types (host/target ASTs) -
kernel terms modules— Hydra-kernel term constants -
kernel terms modules with DeepCore— Encoders/decoders/extractors -
term-level sources outside of the kernel— Per-language term constants (coders, etc.) -
tests— Test groups whose@@is binding application -
term-encoded tests— Test groups whose@@builds Hydra-runtime term applications (lambdas, primitives) -
kernel test fixtures— Named term/type/binding fixtures used by tests
Each category has a canonical comment tag (the names above in backticks) placed above its import block. When creating a new module, copy the import block from an existing module of the same category.
| Category | Convention | Examples |
|---|---|---|
| Functions | camelCase |
normalizeComment, gatherApplications
|
| Predicates |
is prefix |
isSimpleAssignment, isComplexTerm, isTrivialTerm
|
| Types | PascalCase |
Term, Type, Binding, Module
|
| Module names | dotted lowercase |
hydra.coderUtils, hydra.json.decode
|
| Module aliases | short, consistent |
Lists, Maps, Core, Graph (not L, M, C, G) |
| Standard lib aliases | single letter |
Data.Map as M, Data.List as L, Data.Maybe as Y
|
| Name constants | underscore prefix |
_Term, _Type_record, _Person_name
|
In DSL code (pure, kernel-level):
- Use
Eitherto represent computations that can fail. - Never return a default value on failure. Propagate the error.
- Provide context in error messages.
In I/O code (Generation.hs and similar):
- Convert
Eitherresults tofailimmediately. - Include a descriptive prefix:
fail $ "Failed to X: " ++ formatError err. - Never silently catch and discard errors.
- Use
-- |(Haddock style) for function documentation. - Use
--for inline clarification of non-obvious logic. - Comments should explain why, not what, unless the code is genuinely obscure.
Hydra has two kinds of doc text, with different formatting rules:
-
Host-local comments — Haskell
-- |, Java Javadoc, Python docstrings, Scala Scaladoc, etc. Use each host's native conventions:- Haskell: Haddock —
@code@for inline code,'identifier'for cross-references. - Java: Javadoc —
{@code name},{@link name}. - Python: project-native (Sphinx reST, mkdocs Markdown, etc.).
- Scala: Scaladoc —
`code`,[[name]]. - TypeScript: TSDoc —
`code`,{@link name}. - These comments never leave their host source file.
- Haskell: Haddock —
-
Translingual
doc "..."strings — supplied to the Hydra DSL when defining a type, term, or field; flow throughdist/json/into every host's generated code. Use Markdown-style backticks (`name`) for inline code references. Per-host coders are expected to translate backticks into the host's monospace convention or strip them. Don't use Haddock, Javadoc, or other host-specific markup in translingual doc strings.
The cross-reference question (how to link an identifier in a translingual doc string to its declaration) is open — see #433. Until that lands, use plain identifier names (no link markup) inside translingual doc strings; backticks are for code formatting only.
These patterns recur in AI-generated contributions. Avoid them.
-- WRONG: Returns a default instead of failing
lookupOrDefault name graph = fromMaybe emptyTerm (lookupElement name graph)
-- WRONG: Catches an error and continues with degraded behavior
processTerms terms = filter isRight (map processTerm terms) -- Silently drops failures
-- CORRECT: Fail immediately
lookupOrFail name graph = case lookupElement name graph of
Nothing -> fail $ "Element not found: " ++ show name
Just e -> return e-- WRONG: Not alphabetical
definitions = [
toTermDefinition zebra,
toTermDefinition apple,
toTermDefinition mango]
-- CORRECT: Alphabetical
definitions = [
toTermDefinition apple,
toTermDefinition mango,
toTermDefinition zebra]The function definitions that follow the elements list must appear in the same
(alphabetical) order.
-- WRONG: Using a non-standard alias
import qualified Hydra.Dsl.Meta.Lib.Lists as L -- Should be Lists, not L
-- WRONG: Unqualifying a module that should be qualified
import Hydra.Dsl.Meta.Lib.Maps -- Should be qualified as Maps
-- WRONG: Qualifying a module that is conventionally unqualified
import qualified Hydra.Kernel as K -- Should be unqualified- DSL guide -- comprehensive reference for import conventions, operators, and application styles
- Developers -- source code organization and release processes
-
Code organization -- the
packages/,heads/,dist/layout - Adding primitives -- cross-language primitive implementation checklist
- Documentation style guide -- conventions for writing Hydra documentation (separate from code style)