Rules Engine
Host data exposed to mino predicates for declarative business logic.
Full source
This is a self-contained C++ program. Copy it, compile against the mino library, and run it.
/*
* rules_engine.cpp - declarative business rules over host data.
*
* A loan approval system where C++ owns the applicant database and
* mino scripts define the underwriting rules. Rules are pure functions
* over immutable data: no side effects, no mutation of host state.
* Change the approval criteria without recompiling.
*
* Build:
* make
* c++ -std=c++17 -Isrc -o examples/use-cases/rules_engine \
* examples/use-cases/rules_engine.cpp src/[a-z]*.o -lm
*/
#include "mino.h"
#include <cstdio>
#include <vector>
/* ── Expose ────────────────────────────────────────────────────────── */
/* Applicant records are built as mino maps from C++ structs.
* Each map is immutable once created. The script cannot modify
* applicant data through the values it receives. */
struct Applicant {
const char *name;
int age;
int credit_score;
double income;
double debt;
int years_employed;
};
static mino_val_t *make_applicant(mino_state_t *S, const Applicant &a)
{
mino_val_t *ks[6], *vs[6];
ks[0] = mino_keyword(S, "name"); vs[0] = mino_string(S, a.name);
ks[1] = mino_keyword(S, "age"); vs[1] = mino_int(S, a.age);
ks[2] = mino_keyword(S, "credit-score"); vs[2] = mino_int(S, a.credit_score);
ks[3] = mino_keyword(S, "income"); vs[3] = mino_float(S, a.income);
ks[4] = mino_keyword(S, "debt"); vs[4] = mino_float(S, a.debt);
ks[5] = mino_keyword(S, "years-employed"); vs[5] = mino_int(S, a.years_employed);
return mino_map(S, ks, vs, 6);
}
/* ── Script ────────────────────────────────────────────────────────── */
/* Keywords like `:credit-score` act as data accessors.
* Sets like `#{:approved :review}` act as membership predicates.
* The pipeline is flat: each step takes and returns data. */
static const char *script =
";; Debt-to-income ratio.\n"
"(defn dti [applicant]\n"
" (/ (:debt applicant) (:income applicant)))\n"
"\n"
";; Classify a single applicant into a risk tier.\n"
"(defn classify [applicant]\n"
" (let [score (:credit-score applicant)\n"
" ratio (dti applicant)\n"
" years (:years-employed applicant)]\n"
" (cond\n"
" (< score 580) :denied\n"
" (> ratio 0.43) :denied\n"
" (and (>= score 720)\n"
" (<= ratio 0.36)\n"
" (>= years 2)) :approved\n"
" :else :review)))\n"
"\n"
";; Tag each applicant with their decision.\n"
"(defn decide [applicant]\n"
" (assoc applicant :decision (classify applicant)))\n"
"\n"
";; Process all applicants and group by decision.\n"
"(defn process [applicants]\n"
" (->> applicants\n"
" (map decide)\n"
" (group-by :decision)\n"
" (map (fn [[tier apps]]\n"
" [tier (mapv :name apps)]))\n"
" (into (sorted-map))))\n"
"\n"
"(process applicants)\n";
/* ── Embed ─────────────────────────────────────────────────────────── */
int main()
{
mino_state_t *S = mino_state_new();
mino_env_t *env = mino_env_new_default(S);
/* Build applicant data from the C++ side. */
std::vector<Applicant> data = {
{"Alice", 34, 750, 85000.0, 22000.0, 5},
{"Bob", 28, 620, 52000.0, 28000.0, 1},
{"Carol", 45, 810, 120000.0, 35000.0, 12},
{"Dave", 22, 540, 38000.0, 15000.0, 0},
{"Eve", 31, 690, 67000.0, 24000.0, 3},
};
/* Convert to a mino vector. Each record is rooted via mino_ref
* so earlier records survive GC while later ones are allocated. */
std::vector<mino_ref_t *> refs;
for (auto &a : data)
refs.push_back(mino_ref(S, make_applicant(S, a)));
std::vector<mino_val_t *> records;
for (auto *r : refs)
records.push_back(mino_deref(r));
mino_env_set(S, env, "applicants",
mino_vector(S, records.data(), records.size()));
for (auto *r : refs)
mino_unref(S, r);
/* Evaluate the rules script. */
mino_val_t *result = mino_eval_string(S, script, env);
if (result) {
printf("decisions: ");
mino_println(S, result);
} else {
fprintf(stderr, "error: %s\n", mino_last_error(S));
}
mino_env_free(S, env);
mino_state_free(S);
}
Build and run:
c++ -std=c++17 -O2 \
-Imino/src -Imino/src/public -Imino/src/runtime -Imino/src/gc -Imino/src/eval \
-Imino/src/collections -Imino/src/prim -Imino/src/async -Imino/src/interop \
-Imino/src/diag -Imino/src/vendor/imath \
-o use-cases/rules_engine \
use-cases/rules_engine.cpp \
mino/src/public/*.c mino/src/runtime/*.c mino/src/gc/*.c mino/src/eval/*.c \
mino/src/collections/*.c mino/src/prim/*.c mino/src/async/*.c mino/src/interop/*.c \
mino/src/regex/*.c mino/src/diag/*.c mino/src/vendor/imath/*.c \
-lm
./use-cases/rules_engineThe mino script
Keywords like :credit-score act as data accessors. Sets like #{:approved :review} act as membership predicates. The pipeline is flat: each step takes and returns data.
;; Debt-to-income ratio.
(defn dti [applicant]
(/ (:debt applicant) (:income applicant)))
;; Classify a single applicant into a risk tier.
(defn classify [applicant]
(let [score (:credit-score applicant)
ratio (dti applicant)
years (:years-employed applicant)]
(cond
(< score 580) :denied
(> ratio 0.43) :denied
(and (>= score 720)
(<= ratio 0.36)
(>= years 2)) :approved
:else :review)))
;; Tag each applicant with their decision.
(defn decide [applicant]
(assoc applicant :decision (classify applicant)))
;; Process all applicants and group by decision.
(defn process [applicants]
(->> applicants
(map decide)
(group-by :decision)
(map (fn [[tier apps]]
[tier (mapv :name apps)]))
(into (sorted-map))))
(process applicants)