⚑ It's alive!

 _____                _    _
     |  ___| __ __ _ _ __ | | _(_) ___
     | |_ | '__/ _` | '_ \| |/ / |/ _ \
     |  _|| | | (_| | | | |   <| |  __/
     |_|  |_|  \__,_|_| |_|_|\_\_|\___| 

The Frankie Language

A procedural programming language lovingly stitched together from Ruby, Python, R, and Fortran. Zero dependencies. Proudly monstrous.

Ruby syntax Python semantics R statistics Fortran power
Get Started β†’ Download v1.15 Go to the Docs β˜… Star on GitHub

β–Ό scroll

What it can do

Everything you need.
Nothing you don't.

Proudly procedural β€” functions, data, loops, logic. No classes, no self, no inheritance. Runs on Python 3.10+ with zero external dependencies.

❓

Ternary Operator v1.15

score >= 90 ? "A" : "B" β€” right-associative, works in any expression: assignments, interpolation, arguments. No more single-expression if/end blocks.

🎯

Keyword Default Params v1.15

def connect(host, port: 5432, ssl: true) β€” Ruby-style name: default syntax in def. Mixed positional and keyword defaults in the same signature.

πŸ’₯

Splat Multi-Assign v1.15

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.15

Explicit const PI = 3.14 constant keyword. Lambda call fn.(args) now works everywhere β€” store, pass, and call lambdas freely.

πŸ”‘

frankieauth + frankieratelimit v1.15

HTTP Basic Auth and HMAC Bearer tokens via frankieauth stitch. In-memory sliding-window rate limiting via frankieratelimit. Zero dependencies.

πŸ› οΈ

frankiec check + frankiec new v1.15

Lint 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.

🌐

Async Routes + Middleware Stack v1.14

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.14

Fire-and-forget background blocks. Kill slow operations before they hang your server. Zero threading boilerplate, zero dependencies.

🎨

frankietemplate β€” Mustache Templates v1.14

Mustache-compatible engine as a stitch. {{ var }}, sections, partials, inverted blocks. render_file("./views/index.html", data). Zero dependencies, pure .fk.

πŸͺ

frankiecookie + Static Files v1.14

HMAC-signed cookies via Python's hmac stdlib β€” tamper-proof, zero deps. app.static("./public") serves a directory in one line.

πŸ—‚οΈ

Hash Destructuring + Shape Matching v1.14

{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.1

Rewritten 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.1

Float 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.

πŸͺ

Web Session Helper + fmt Idempotency v1.13.1

Cookie-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.13

Zero-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.12

Transform 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.12

Re-run on save. repl --no-banner for scripts. rescue FileNotFoundError now works. Type errors speak Frankie, not Python internals.

↩️

Implicit Return + Inline if v1.11

Last expression returned automatically. grade = if score >= 90 then "A" else "B" end. The ergonomics new users expect from day one.

✳️

String & Vector * + Heredoc v1.10

"ha" * 3 β†’ "hahaha". Heredoc <<~TEXT with auto indent-stripping and full interpolation.

πŸ“‹

Record Types v1.9

record Point(x, y) β€” named data objects. Every hash method and iterator works instantly.

Ξ»

Lambdas ->(x) { } v1.8

Store and pass functions as first-class values. Default params, closures, higher-order functions.

πŸ›‘οΈ

Nil Safety &.

x&.method returns nil instead of crashing. Chains short-circuit at the first nil.

πŸ“Š

R-style Statistics

Built-in mean, stdev, median, variance. Pipe operator |> for clean data pipelines.

🌐

Built-in Web Server

Sinatra-style routing. Path params, JSON bodies, filters, custom 404 β€” zero external deps.

πŸ—„οΈ

SQLite + HTTP + JSON + CSV

All stdlib, all zero-dependency. db_open, http_get, json_read, csv_write.

πŸ§ͺ

frankiec test

Built-in test runner. assert_eq, assert_match, assert_raises_typed. Live βœ“/βœ—, exits 1 on failure.

The language

Familiar. Expressive.
A little monstrous.

nil_safety.fk v1.7
# &. β€” 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!
stats.fk
# 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.fk v1.8
# 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
records.fk v1.9
# 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
v1.15.fk v1.15
# 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
v1.14.fk v1.14
# 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
v1.13.1.fk v1.13.1
# 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()
v1.13.fk v1.13
# 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
v1.12.fk v1.12
# .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
v1.11.fk v1.11
# 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])
v1.10.fk v1.10
# 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]
tooling.fk v1.9
## 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

Build APIs in Frankie.
No framework required.

Sinatra-inspired routing built into the language. Define routes with blocks, handle JSON, path params, filters and custom 404s β€” all zero external dependencies.

webapp.fk
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 helpers: response(body)   html_response(html)   json_response(data)   redirect(url)   halt(status, body)

The family resemblance

One program,
five languages.

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
Ruby
Python
R
Fortran
stats_report.fk
# 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
do…end blocks from Ruby Β· mean / stdev / abs from R Β· // integer division from Fortran Β· hash as dict from Python Β· #{} interpolation from Ruby Β· select / each_with_object from Ruby
stats_report.rb
# 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
Frankie borrows from Ruby: do…end blocks Β· #{} interpolation Β· select / each_with_object Β· .min / .max / .sum Β· destructuring assignment Β· puts
stats_report.py
# 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}")
Frankie borrows from Python: clean execution model Β· dict as hash Β· list as vector Β· zero-dependency philosophy Β· round() / abs() / min() / max() Β· f-string β†’ #{} interpolation
stats_report.R
# 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))
}
Frankie borrows from R: mean() / stdev() / min() / max() Β· vectors as first-class values Β· seq() / linspace() Β· pipe operator |> Β· statistical mindset Β· abs() on collections
stats_report.f90
! 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
Frankie borrows from Fortran: // integer division Β· ** exponentiation Β· do…while loop structure Β· numeric pragmatism Β· sum() / sqrt() on arrays Β· computational heritage

The donors

Stitched from four
legendary languages.

Ruby
  • do…end syntax
  • if / unless
  • String interpolation
  • begin / rescue
  • Iterators & blocks
  • Postfix conditions
Python
  • Clean semantics
  • Dict / list model
  • Execution model
  • Rich data types
  • Standard library
  • Zero-dep philosophy
R
  • Vectors
  • Pipe operator |>
  • mean / stdev / median
  • seq / linspace
  • Vectorised arithmetic
  • Statistical mindset
Fortran
  • do…while loops
  • Integer division //
  • Exponentiation **
  • Numeric heritage
  • Loop pragmatism
  • Computational roots

Get started

Up and running
in 30 seconds.

Requirements: Python 3.10+. That's it.

01
# Clone the repo
git clone https://github.com/atejada/Frankie && cd Frankie
02
# Install (creates bin/frankiec)
python3 install.py
export PATH="/path/to/Frankie/bin:$PATH"
03
# Run a program, launch the REPL, or run your tests
frankiec run examples/hello.fk
frankiec repl
frankiec test
VersionHighlightsStatus
v1.15ternary 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/newLatest
v1.14spawn/timeout, async routes, middleware, static files, frankietemplate, frankiecookie, hash destructuring, shape pattern matching, scaffold stitchesStable
v1.13.1frankiestring v2, .sum do / .flat_map do fixes, assert_approx_eq, run_tests() public, session(req,resp), fmt blank lines + idempotencyStable
v1.13stitch keyword, frankieforms/frankitable/frankiecolor/frankiepager/frankieconfig/frankiestring, ? in function namesStable
v1.12gsub block, map_hash, round(), .product, .chars docs, FileNotFoundError fix, assert_match/nil, frankiec watch, repl --no-banner, friendlier errorsStable
v1.11Implicit return, inline if expression, .replace(), .format(hash), .zip_with, multi-line REPL history, boxed check errorsStable
v1.10String/vector *, heredoc <<~, times(), flatten(depth), map_with_index, pp, encode/decode, exit codes, --helpStable
v1.9Records, dig, zip(), frankiec fmt, frankiec docs, readline REPL, .env loaderOld
v1.8Lambdas ->(x) { }, hash merge |, group_by, each_slice, each_consStable
v1.7Nil safety &., string templates, file system ops, assert_raises_typedOld
v1.6Compound assign +=/-=/*= etc., typed rescue, .find/.detect, frankiec testOld
v1.5next / break, constants, randomness, sort_by, sleep, unzipOld
v1.4Built-in web server, routes, JSON API, filters, custom 404Old
v1.3JSON, CSV, DateTime, HTTP client, project scaffoldingOld
v1.2SQLite built-in, transactions, multi-file with requireOld
v1.1Rich iterators, REPL, case/when, destructuringOld
v1.0Initial release β€” core language, stdlib, error handlingOld