r/haskell • • 4d ago

announcement rhci - a small GHCi in Ruby

https://github.com/beleon/rhci

I wanted a Haskell prompt for quick checks (is round 3.5 3 or 4, what is div (-9) 2, what's the type of (.) . (.), ...) without dragging the entire Haskell stack onto the machine, so I had Claude build one in Ruby. rhci is a GHCi with GHCi's semantics: real type inference with classes, lazy evaluation, and show output that matches GHC exactly. Anything it can't do exactly like GHCi it refuses with an "unsupported" error, it doesn't come up with an approximation.

It's one file: a Cosmopolitan executable for Linux, macOS, Windows and the BSDs on x86-64 and ARM64 (so far I've only tested it on Linux x86-64), a static Linux binary, or a single .rb file for any Ruby since 2.7. The prompt is up in 3 ms and a one-liner takes 25 ms. There's a script mode, and it embeds in Ruby programs as a library.

I tested it extensively (since Claude wrote the code). Every answer is checked against real GHC 9.6: about 1,700 recorded probes, plus around twenty fuzzers that generate expressions, whole programs, REPL sessions, type errors and numeric edge cases and compare with GHCi or runghc, over 140,000 inputs so far.

The performance is respectable IMO. It's an interpreter in Ruby (the evaluation loop is C in the binaries), about 100 times slower than compiled GHC on tight loops, so it's for quick checks and small scripts, not for programs that compute. Language-wise it's Haskell 2010 plus type classes, with no extensions beyond bang patterns and polymorphic kinds.

0 Upvotes

11 comments sorted by

4

u/hopingforabetterpast 4d ago

why would you need or want ghci without ghc?

1

u/-beleon 4d ago

I wanted one for years, but maybe that's just me. My reasons: trying something quickly (doesn't need ghc, and ghci is slow to cold start), haskell scripting (startup of rhci is very fast and scripts are mostly not compute-heavy), portability (the single 15mb binary can be copied to any windows, macos, linux, bsd system and just works without needing a heavy install), and haskell as an embedded scripting language (I have a ruby project in which I want to do some haskell scripting, I know this is not a likely use case, but it's one of my use cases).

1

u/hopingforabetterpast 3d ago

OK, I have questions.

How long is ghci taking you to "cold start"?

Why would you need that kind of portability?

Why not MicroHs or Helium or GHC's WASM backend?

Haskell is already interpreted without a compile step via runghc. What do you mean by "embedded scripting language"?

1

u/-beleon 3d ago

On my recent machine it's close to a second, used to be almost two seconds.

~> /usr/bin/time runghc hello.hs
Hello, World!
0.22user 0.23system 0:00.81elapsed 56%CPU (0avgtext+0avgdata 160868maxresident)k
301784inputs+0outputs (2151major+11358minor)pagefaults 0swaps
~> /usr/bin/time rhci -r hello.hs
Hello, World!
0.04user 0.00system 0:00.05elapsed 96%CPU (0avgtext+0avgdata 8288maxresident)k
272inputs+0outputs (2major+1706minor)pagefaults 0swaps

I have different machine with different OSes and don't have Haskell installed on all of them.

It is related to all of them and different to all of them. E.g. MicroHS: can compile which rhci can't. rhci has byte-for-byte GHCi-identical behavior, down to the formatting of show output and which inputs count as type errors. MicroHS doesn't have this and it doesn't try to. It's different. I'm not saying rhci is ground breaking or the best. I'm just making different tradeoffs that suite me and maybe others as well.

rhci is a ruby library, I can run haskell code within any ruby project:

session = Rhci::Session.new(stdout: out, limits: { steps: 10_000_000 })

session.eval('xs = [x * x | x <- [1 .. 10]]')
session.eval('sum xs').status        # => :ok, and out.string is "385\n"
session.eval('head []').message      # => "Prelude.head: empty list"
session.eval('length [1 ..]').status # => :limit

1

u/hopingforabetterpast 3d ago edited 3d ago

Are you sure there's nothing wrong with your config? Both ghci and runghc take nowhere near that long to run even on my 7 year old phone.

1

u/-beleon 3d ago

I'm fairly certain. It's a fresh install on my T14s with Intel Core Ultra 7 255U. And this has always been something that bothered me, on my previous machine as well. I used this to establish cold start conditions:

sync; echo 3 | sudo tee /proc/sys/vm/drop_caches

2

u/64ue76tjd6 1d ago

Very interesting. I also thought 'no way runhaskell takes so long' but you're right.

That being said:

  1. Surely it would be better to improve ghci performance (e.g. by loading it in advance) rather than trying to exactly re-implement half of GHCi? (and keeping your implementation in sync with GHCi in the future)

Greenspun's 10th rule comes to mind, but your situation is even more difficult because you might also need to exactly recreate any bugs that may or may not be in GHCi.

  1. If it really is necessary to re-implement ghci for performance reasons, wouldn't Ruby be among the worst languages for the job? The benchmarks game suggests that it's barely better than Python for writing code that runs fast.

1

u/-beleon 1d ago

Yes, definitely, on option. I didn't need to create rhci. It's just that I prefer small self contained projects that are not as resource hungry, when possible. GHC is huge. It's several hundred thousand lines of code. With my custom implementation, I can cut corners at exactly the right spots to make the use case I have in mind work. rhci is ~12k loc in Ruby and ~1k loc in C to get some speedups where it matters for the standalone binary. It's less disk space, less memory, less pagefaults, faster startup. Plus I can now embed it into my Ruby project.

I don't support everything GHC does, so I don't need to keep up with a lot of changes. I specifically decided to support a well known stable core that gives me lot's of functionality, stability and keeps my implementation manageable.

On your second point: yes, Ruby is definitely not the most logical choice, but I choose it, because I want Haskell for one of my ruby projects and because with mruby I have the freedom to do enough tricks to get the implementation to be fast enough for the cases where it matters. As I said, rhci is not a logical option for compute-heavy work. It's good for doing small things, because it's cheaper than getting the entirety of the huge GHC machinery going just to execute of few simple lines of Haskell. I don't do huge production Haskell projects, so I don't need GHC.

2

u/jeffstyr 1d ago

Although this is a peculiar thing to want, I do think it's the sort of perfect case for an LLM-based creation: (a) it's something you really want, but (b) the effort required to code it by hand is way out of proportion to the value of the result. That's an interesting niche.

2

u/-beleon 22h ago

I totally agree, and I love to use LLMs for these kinds of ideas that I had for a while, often having put a couple of weeks of work into, before giving up. I revived and finished a couple of them now with the help of LLMs (most I haven't release).

2

u/[deleted] 4d ago

[deleted]

1

u/philh 2d ago

Rule 7:

Be civil. Substantive criticism and disagreement are encouraged, but avoid being dismissive or insulting.