β‘ It's alive!
_____ _ _
| ___| __ __ _ _ __ | | _(_) ___
| |_ | '__/ _` | '_ \| |/ / |/ _ \
| _|| | | (_| | | | | <| | __/
|_| |_| \__,_|_| |_|_|\_\_|\___|
A procedural programming language lovingly stitched together from Ruby, Python, R, and Fortran. Zero dependencies. Proudly monstrous.
βΌ scroll
What it can do
Proudly procedural β functions, data, loops, logic. No classes, no self, no inheritance. Runs on Python 3.10+ with zero external dependencies.
score >= 90 ? "A" : "B" β right-associative, works in any expression: assignments, interpolation, arguments. No more single-expression if/end blocks.
def connect(host, port: 5432, ssl: true) β Ruby-style name: default syntax in def. Mixed positional and keyword defaults in the same signature.
a, b, *rest = [1,2,3,4,5] and first, *mid, last = vec β capture remaining elements anywhere in a destructuring assignment.
const + fn.(args) v1.15Explicit const PI = 3.14 constant keyword. Lambda call fn.(args) now works everywhere β store, pass, and call lambdas freely.
frankieauth + frankieratelimit v1.15HTTP Basic Auth and HMAC Bearer tokens via frankieauth stitch. In-memory sliding-window rate limiting via frankieratelimit. Zero dependencies.
frankiec check + frankiec new v1.15Lint without running β CI-ready frankiec check file.fk exits 1 on errors. Full project scaffold with frankiec new myapp β runs and tests pass out of the box.
app.get_async for non-blocking handlers. app.use do |req, next_fn| for chainable auth, logging, and rate-limiting. Real web apps, real patterns.
spawn { } + timeout(n) { } v1.14Fire-and-forget background blocks. Kill slow operations before they hang your server. Zero threading boilerplate, zero dependencies.
frankietemplate β Mustache Templates v1.14Mustache-compatible engine as a stitch. {{ var }}, sections, partials, inverted blocks. render_file("./views/index.html", data). Zero dependencies, pure .fk.
frankiecookie + Static Files v1.14HMAC-signed cookies via Python's hmac stdlib β tamper-proof, zero deps. app.static("./public") serves a directory in one line.
{name, age} = user pulls keys into variables. case user when {role: "admin"} matches on hash shape. Data manipulation finally feels natural.
frankiestring v2 + Vector block fixes v1.13.1Rewritten string stitch with pad_left, pad_right, truncate, slugify, word_wrap, indent_lines. Plus .sum do, .flat_map do multi-line blocks now work correctly.
assert_approx_eq + run_tests() v1.13.1Float comparison in tests with configurable delta (default 0.001). run_tests() is now a public stdlib function β callable from any .fk file, not just frankiec test.
fmt Idempotency v1.13.1Cookie-backed session(req, resp) β read, mutate, .save(). Zero server state. frankiec fmt now preserves blank lines, expands long hashes, and is safe for pre-commit hooks.
stitch "name" β Package System v1.13Zero-dependency, zero-registry packages. Drop a .fk file in ./stitches/ and stitch it in. Resolves locally then ~/.frankie/stitches/. No lockfile, no network.
def even?(n) β Predicate Names v1.13? in user-defined function names now works. Write valid?, palindrome? just like Ruby. Compiled transparently to _q in generated Python.
.gsub block + .map_hash + round() v1.12Transform each regex match with a block. Map a hash to a new hash in one step. Round to N decimal places. Three long-overdue stdlib additions.
frankiec watch + friendlier errors v1.12Re-run on save. repl --no-banner for scripts. rescue FileNotFoundError now works. Type errors speak Frankie, not Python internals.
if v1.11Last expression returned automatically. grade = if score >= 90 then "A" else "B" end. The ergonomics new users expect from day one.
* + Heredoc v1.10"ha" * 3 β "hahaha". Heredoc <<~TEXT with auto indent-stripping and full interpolation.
record Point(x, y) β named data objects. Every hash method and iterator works instantly.
->(x) { } v1.8Store and pass functions as first-class values. Default params, closures, higher-order functions.
&.x&.method returns nil instead of crashing. Chains short-circuit at the first nil.
Built-in mean, stdev, median, variance. Pipe operator |> for clean data pipelines.
Sinatra-style routing. Path params, JSON bodies, filters, custom 404 β zero external deps.
All stdlib, all zero-dependency. db_open, http_get, json_read, csv_write.
Built-in test runner. assert_eq, assert_match, assert_raises_typed. Live β/β, exits 1 on failure.
The language
# &. β nil-safe navigation operator user = {name: "Alice", age: 30} missing = nil puts user["name"]&.upcase # ALICE puts missing&.upcase # nil puts missing&.upcase&.reverse # nil def greet(name) display = name&.upcase if display == nil puts "Hello, stranger!" else puts "Hello, #{display}!" end end greet("bob") # Hello, BOB! greet(nil) # Hello, stranger!
# R-style stats + pipe operator data = [23, 45, 12, 67, 34, 89] puts mean(data) # 45.0 puts stdev(data) # 27.9 puts median(data) # 39.5 data |> sum |> puts # 270 top3 = data.sort_by do |x| -x end.take(3) puts top3 # [89, 67, 45] # Iterators words = ["banana", "fig", "cherry"] long = words.select do |w| w.length > 4 end puts long # [banana, cherry] [1,2,3,4,5,6].each do |n| next if n % 2 == 0 break if n > 5 puts n # 1 3 5 end
# Lambdas β first-class functions double = ->(x) { x * 2 } add = ->(a, b) { a + b } puts double.call(7) # 14 puts add.call(3, 4) # 7 def apply(fn, val) return fn.call(val) end puts apply(double, 9) # 18 # Hash merge | and group_by cfg = {color: "blue"} | {color: "red", size: "md"} puts cfg["color"] # red nums = [1,2,3,4,5,6] by_p = nums.group_by do |n| n % 2 == 0 ? "even" : "odd" end puts by_p["even"] # [2, 4, 6] [1,2,3,4].each_slice(2) do |s| puts s end
# record β lightweight named data objects record Point(x, y) record Employee(name, dept, salary) p1 = Point(3, 4) emp = Employee("Alice", "Eng", 95000) puts p1 # Point(x: 3, y: 4) puts emp["dept"] # Eng # dig β safe nested access cfg = {db: {host: "localhost", port: 5432}} puts cfg.dig("db", "host") # localhost puts cfg.dig("db", "missing") # nil # zip() standalone + .env auto-load zip(["a","b"], [1,2]).each do |p| puts "#{p[0]}:#{p[1]}" end # frankiec fmt / docs / readline REPL db = env("DB_PATH", "app.db") puts db
# Ternary operator grade = score >= 90 ? "A" : score >= 70 ? "B" : "C" # Keyword default parameters def connect(host, port: 5432, ssl: true) puts "#{host}:#{port} ssl=#{ssl}" end # Splat multi-assign first, *mid, last = [1, 2, 3, 4, 5] # const keyword const MAX = 100 # Lambda call syntax everywhere double = ->(x) { x * 2 } puts double.(6) # frankieauth stitch stitch "frankieauth" app.use do |req, next_fn| if not basic_auth_ok?(req, "admin", env("PASS")) halt(401, "Unauthorized") end next_fn.(req) end # frankiec check β CI lint # $ frankiec check app.fk β exit 0 or 1 # frankiec new myapp β full scaffold
# Hash destructuring user = {name: "Alice", role: "admin", age: 30} {name, role} = user puts "#{name} is a #{role}" # Shape pattern matching case user when {role: "admin"} puts green("Admin access granted") when {role: "moderator"} puts yellow("Moderator tools") end # Mustache templates stitch "frankietemplate" html = render_file("./views/index.html", {title: "Home", user: user}) # spawn β fire and forget spawn do send_welcome_email(user["email"]) end # Middleware app.use do |req, next_fn| puts "β #{req.method} #{req.path}" next_fn.(req) end
# frankiestring v2 β clean string helpers stitch "frankiestring" puts pad_left("42", 6, "0") # "000042" puts slugify("Hello World!") # "hello-world" puts word_wrap("The quick brown fox", 12) # .sum do β projected sum in one step cart = [{price: 0.99, qty: 4}, {price: 2.49, qty: 2}] total = cart.sum do |item| item["price"] * item["qty"] end puts total # 8.94 # assert_approx_eq β float testing assert_approx_eq(sqrt(2.0), 1.4142, 0.0001, "sqrt(2)") run_tests() # Cookie-backed session β no server state s = session(req, resp) s["count"] = (s["count"] or 0) + 1 s.save()
# stitch β drop a .fk file in ./stitches/ and load it by name stitch "frankieforms" stitch "frankietable" stitch "frankiecolor" # Form validation rules = {email: [{rule: "required"}, {rule: "email"}]} puts valid?({email: "[email protected]"}, rules) # true # ASCII tables rows = [{name: "Alice", score: 95}, {name: "Bob", score: 87}] puts table(rows) # Terminal colors puts red("Error!") puts green("Done!") puts success("All tests passed") # ? in function names β now works def even?(n) n % 2 == 0 end puts even?(4) # true
# .gsub with block β transform each match puts "hello world".gsub(/[aeiou]/) do |m| m.upcase end # hEllO wOrld # .map_hash β transform a hash into a new hash prices = {apple: 1.20, banana: 0.50} puts prices.map_hash do |k, v| [k, v * 2] end # round(x, n) β finally puts round(3.14159, 2) # 3.14 # .product β cartesian product puts [1,2].product([3,4]) # [[1,3],[1,4],[2,3],[2,4]] # rescue FileNotFoundError now actually works begin file_read("missing.txt") rescue FileNotFoundError e puts "Caught: #{e}" end # frankiec watch main.fk β re-run on save # frankiec repl --no-banner β headless REPL
# Implicit return β no 'return' needed def double(x) x * 2 end puts double(7) # 14 # Inline if expression grade = if score >= 90 then "A" else "B" end # String .replace() and .format(hash) puts "hello world".replace("world", "Frankie") puts "Hi, {name}!".format({name: "Alice"}) # .zip_with β pair-wise transform puts [1,2,3].zip_with([10,20,30]) do |a, b| a + b end # [11, 22, 33] # Multiple return values def minmax(v) [min(v), max(v)] end lo, hi = minmax([3,1,4,1,5,9])
# String & vector * repetition puts "ha" * 3 # hahaha puts [0] * 5 # [0, 0, 0, 0, 0] # Heredoc <<~ strips indent + interpolates v = "1.10" sql = <<~SQL SELECT * FROM users WHERE version = '#{v}' SQL puts sql # times() standalone + map_with_index times(3) do |i| puts "tick #{i}" end puts ["a","b","c"].map_with_index do |v, i| "#{i}:#{v}" end # pp + encode/decode pp({host: "localhost", port: 3000}) puts "hi".encode # [104, 105]
## Distance between two points. ## @param p1 A Point record ## @param p2 A Point record ## @return Float distance def distance(p1, p2) dx = p1["x"] - p2["x"] dy = p1["y"] - p2["y"] return sqrt(dx*dx + dy*dy) end # frankiec docs generates Markdown # frankiec fmt --write myfile.fk # frankiec fmt --check (CI mode) # frankiec --help / frankiec run --help # Named rescue without variable: begin x = 1 // 0 rescue ZeroDivisionError puts "caught" end
Web server
Sinatra-inspired routing built into the language. Define routes with blocks, handle JSON, path params, filters and custom 404s β all zero external dependencies.
app = web_app() app.get("/") do |req| html_response("<h1>Hello from Frankie! π§</h1>") end app.get("/greet/:name") do |req| name = req.params["name"] response("Hello, #{name}!") end app.get("/api/status") do |req| json_response({status: "ok", version: "1.8"}) end store = {notes: []} app.post("/notes") do |req| data = req.json if data == nil return halt(400, "Expected JSON") end note = {id: length(store["notes"]) + 1, text: data["text"]} store["notes"].push(note) json_response(note, 201) end app.not_found do |req| halt(404, "No route for #{req.path}") end app.run(3000) # π§ Frankie web server running on http://0.0.0.0:3000
response(body)
html_response(html)
json_response(data)
redirect(url)
halt(status, body)
The family resemblance
The same program β read a list of numbers, compute stats, find outliers, print a report β written in Frankie and each of its four donor languages. Spot what Frankie borrows from each one.
# Frankie β stats report # Syntax from Ruby Β· semantics from Python Β· stats from R Β· // from Fortran data = [12, 45, 7, 83, 29, 56, 14, 91, 38, 62] avg = mean(data) sd = stdev(data) lo, hi = min(data), max(data) puts "ββ Stats Report ββββββββββββββββββ" puts "Count : #{data.length}" puts "Mean : #{round(avg, 2)}" puts "Stdev : #{round(sd, 2)}" puts "Range : #{lo} β #{hi}" puts "" # Outliers: more than 1 stdev from mean (Ruby-style block Β· R-style stdev) outliers = data.select do |x| abs(x - avg) > sd end puts "Outliers (Β±1Ο): #{outliers}" puts "" # Histogram buckets using integer division (Fortran heritage: //) buckets = data.each_with_object({}) do |x, h| key = ((x // 20) * 20).to_s + "s" cur = h[key] h[key] = (if cur == nil then 0 else cur end) + 1 end buckets.each do |k, v| puts " #{k.rjust(4)} β #{"β" * v} #{v}" end
# Ruby β stats report data = [12, 45, 7, 83, 29, 56, 14, 91, 38, 62] avg = data.sum.to_f / data.length sd = Math.sqrt(data.sum { |x| (x - avg) ** 2 } / data.length) lo, hi = data.min, data.max puts "ββ Stats Report ββββββββββββββββββ" puts "Count : #{data.length}" puts "Mean : #{avg.round(2)}" puts "Stdev : #{sd.round(2)}" puts "Range : #{lo} β #{hi}" puts "" # Outliers: more than 1 stdev from mean outliers = data.select { |x| (x - avg).abs > sd } puts "Outliers (Β±1Ο): #{outliers}" puts "" # Histogram buckets buckets = data.each_with_object(Hash.new(0)) do |x, h| key = "#{(x / 20) * 20}s" h[key] += 1 end buckets.each do |k, v| puts " #{k.rjust(4)} β #{"β" * v} #{v}" end
# Python β stats report import math data = [12, 45, 7, 83, 29, 56, 14, 91, 38, 62] avg = sum(data) / len(data) sd = math.sqrt(sum((x - avg) ** 2 for x in data) / len(data)) lo, hi = min(data), max(data) print("ββ Stats Report ββββββββββββββββββ") print(f"Count : {len(data)}") print(f"Mean : {round(avg, 2)}") print(f"Stdev : {round(sd, 2)}") print(f"Range : {lo} β {hi}") print("") # Outliers: more than 1 stdev from mean outliers = [x for x in data if abs(x - avg) > sd] print(f"Outliers (Β±1Ο): {outliers}") print("") # Histogram buckets buckets = {} for x in data: key = f"{(x // 20) * 20}s" buckets[key] = buckets.get(key, 0) + 1 for k, v in buckets.items(): print(f" {k:>4} β {'β' * v} {v}")
# R β stats report data <- c(12, 45, 7, 83, 29, 56, 14, 91, 38, 62) avg <- mean(data) sd <- sd(data) lo <- min(data) hi <- max(data) cat("ββ Stats Report ββββββββββββββββββ\n") cat(sprintf("Count : %d\n", length(data))) cat(sprintf("Mean : %.2f\n", avg)) cat(sprintf("Stdev : %.2f\n", sd)) cat(sprintf("Range : %d β %d\n\n", lo, hi)) # Outliers: more than 1 stdev from mean (vectorised β no loop needed) outliers <- data[abs(data - avg) > sd] cat("Outliers (Β±1Ο):", outliers, "\n\n") # Histogram buckets using integer division keys <- paste0((data %/% 20) * 20, "s") buckets <- table(keys) for (k in names(buckets)) { v <- buckets[[k]] cat(sprintf(" %4s β %s %d\n", k, strrep("β", v), v)) }
! Fortran 90 β stats report program stats_report implicit none integer, parameter :: n = 10 real :: data(n) = [12,45,7,83,29,56,14,91,38,62] real :: avg, sd, lo, hi, diff integer :: i, bucket, counts(5) avg = sum(data) / n sd = sqrt(sum((data - avg)**2) / n) lo = minval(data) hi = maxval(data) print *, "ββ Stats Report ββββββββββββββββββ" print '("Count : ",I0)', n print '("Mean : ",F6.2)', avg print '("Stdev : ",F6.2)', sd print '("Range : ",F0.0," β ",F0.0)', lo, hi print *, "" print *, "Outliers (Β±1Ο):" do i = 1, n diff = abs(data(i) - avg) if (diff > sd) print *, " ", int(data(i)) end do print *, "" ! Histogram buckets with integer division (//) counts = 0 do i = 1, n bucket = (int(data(i)) / 20) + 1 if (bucket <= 5) counts(bucket) = counts(bucket) + 1 end do do i = 1, 5 print '(" ",I2,"0s β ",A," ",I0)', (i-1)*2, & repeat("β", counts(i)), counts(i) end do end program stats_report
The donors
Get started
Requirements: Python 3.10+. That's it.
# Clone the repo
git clone https://github.com/atejada/Frankie && cd Frankie# Install (creates bin/frankiec)
python3 install.py
export PATH="/path/to/Frankie/bin:$PATH"# Run a program, launch the REPL, or run your tests
frankiec run examples/hello.fk
frankiec repl
frankiec test| Version | Highlights | Status |
|---|---|---|
| v1.15 | ternary operator, keyword default params, splat multi-assign, const keyword, fn.(args) everywhere, json_encode, base64/hmac public, String#format, path helpers, date arithmetic, frankieauth, frankieratelimit, frankiec check/new | Latest |
| v1.14 | spawn/timeout, async routes, middleware, static files, frankietemplate, frankiecookie, hash destructuring, shape pattern matching, scaffold stitches | Stable |
| v1.13.1 | frankiestring v2, .sum do / .flat_map do fixes, assert_approx_eq, run_tests() public, session(req,resp), fmt blank lines + idempotency | Stable |
| v1.13 | stitch keyword, frankieforms/frankitable/frankiecolor/frankiepager/frankieconfig/frankiestring, ? in function names | Stable |
| v1.12 | gsub block, map_hash, round(), .product, .chars docs, FileNotFoundError fix, assert_match/nil, frankiec watch, repl --no-banner, friendlier errors | Stable |
| v1.11 | Implicit return, inline if expression, .replace(), .format(hash), .zip_with, multi-line REPL history, boxed check errors | Stable |
| v1.10 | String/vector *, heredoc <<~, times(), flatten(depth), map_with_index, pp, encode/decode, exit codes, --help | Stable |
| v1.9 | Records, dig, zip(), frankiec fmt, frankiec docs, readline REPL, .env loader | Old |
| v1.8 | Lambdas ->(x) { }, hash merge |, group_by, each_slice, each_cons | Stable |
| v1.7 | Nil safety &., string templates, file system ops, assert_raises_typed | Old |
| v1.6 | Compound assign +=/-=/*= etc., typed rescue, .find/.detect, frankiec test | Old |
| v1.5 | next / break, constants, randomness, sort_by, sleep, unzip | Old |
| v1.4 | Built-in web server, routes, JSON API, filters, custom 404 | Old |
| v1.3 | JSON, CSV, DateTime, HTTP client, project scaffolding | Old |
| v1.2 | SQLite built-in, transactions, multi-file with require | Old |
| v1.1 | Rich iterators, REPL, case/when, destructuring | Old |
| v1.0 | Initial release β core language, stdlib, error handling | Old |