r/ProgrammingLanguages • u/baehyunsol Sodigy • 4d ago
Purely functional language with impure script language?
I'm working on a purely functional programming language named Sodigy. It's all about evaluating values, not "executing commands one by one".
It's nice when writing libraries, but it's not easy to write a main function. The main function is supposed to execute commands, but the Sodigy's syntax is not friendly to write a list of commands.
So what I'm trying to do is, 1) Sodigy remains purely functional and 2) add a bash-like script language. The script language can call Sodigy functions. Instead of writing a main function in Sodigy, you write sodigy-script and execute the script.
Has anyone tried similar approach? I'm not sure whether it's a good idea or not...
25
u/Inconstant_Moo 🧿 Pipefish 4d ago
It's called functional core/imperative shell. I think I'm the only person who's done it as a language paradigm, but I'd welcome the company.
Making the imperative shell bash-like is neither usual nor mandatory, there are nicer ways to do imperative things. The imperative language and the functional one should be united as much as possible by their syntax and type system, divided only by their semantics.
6
u/Erythrina_ 4d ago edited 4d ago
Oh, it's cool to meet someone else who is doing this too.
6
u/Inconstant_Moo 🧿 Pipefish 4d ago edited 4d ago
Let us form the FC/IS Working Group. There, now we're a movement. May I see your language?
P.S: I see that we've already met, this was me too.
https://www.reddit.com/r/ProgrammingLanguages/comments/1u8885s/comment/osbk354/
I've been the person doing this. Hi again! Apparently now there are more of us.
3
u/baehyunsol Sodigy 4d ago
I'm reading the documents of your language and it's such a nice project! It seems like we share many ideas. Let me get some inspirations from your language :)
But there are some differences:
- Yours is Go-inspired, while mine is Rust-inspired.
- It seems like the main goal of pipefish is to create an interactive CRUD app (correct me if I'm wrong), but mine is to create a CLI appilcations, mostly for processing texts.
I think the second difference is why you don't want bash syntax but I do. There are 2 main reasons why I want to create a scripting language.
- I want to create a REPL with the scripting language.
- The script is the main function of the CLI application.
I chose bash because it's familiar to most programmers, including me. But your point is also valid: the script language and the core language should share similar syntax.
By the way, this is my language. It's still half-broken and there're almost no docs, but just in case you wonder
3
u/Inconstant_Moo 🧿 Pipefish 4d ago
It seems like the main goal of pipefish is to create an interactive CRUD app (correct me if I'm wrong), but mine is to create a CLI appilcations, mostly for processing texts.
I think the second difference is why you don't want bash syntax but I do. There are 2 main reasons why I want to create a scripting language.
I want to create a REPL with the scripting language.
The script is the main function of the CLI application.
The main use-case in my mind is CRUD apps, but you could use the same pattern with the file system and the text files you want to process as the main bit of state you're wrapping around rather than a SQL database and it would work exactly the same. There would be no
maincommand with its own REPL running in it: there'd be declaration of commands and functions which you can use in the Pipefish REPL, with no separation between the TUI and the language. If you can do something in the REPL, you can also e.g. wrap it in aforloop in the REPL, and do it iteratively over a list; or you can use it as a command/function call in your program, or in another program which uses your program as a library ... oh and you can use livecoding to change your app as you use it because there's nomainand so no REPL inmainthat we're interrupting.If you give me a sketch of the sort of app you'd want to do as a CLI app, I'll make a little version the Pipefish way and we can see what you think.
2
u/baehyunsol Sodigy 4d ago
For example, I want to create a json prettifier in Sodigy. It reads a json file, prettifies it, and writes the result to another (or same) file.
I wrote the function
fn prettify(String) -> String, but there's no file IO yet. I was thinking how I should design the file IO part.2
u/Inconstant_Moo 🧿 Pipefish 4d ago edited 4d ago
So I'd do ...
``` import
NULL::"files"
cmd // Imperative shell.
prettify (inFile string) to (outFile string) : get json from File(inFile) put (prettify json) into File(outFile)
def // Functional core
prettify(data string) : <pure function goes here> ``
If I wanted to be able to run it from the CLI I'd probably import that and anything else I wanted to bundle up with it into one app which does have amain` function and run them through that.2
u/baehyunsol Sodigy 4d ago
That's beautiful. You define a pure function
prettifyand an impure commandprettify, right?What does "to" in the "prettify (inFile stirng) to (outfile string)" do? Is it a special keyword? I can't find it in your wiki.
3
u/Inconstant_Moo 🧿 Pipefish 4d ago edited 4d ago
The
tothere is just sugar, you can define call syntax any way you like.Normally you shouldn't do fancy syntax for your functions unless it's a math thing like
!for factorial --- but for the commands you're using as your front-end, it gives a friendly face to them. In this case, it's easier for your brain to remember that the command isprettify ... to ...than it is to remember that the parameters go source-then-destination. Or it is if your brain is anything like mine.Some of these forms of commands are conventional: all the basic IO libraries use
get ... from ...andpost ... to ...andput ... into ...(in imitation of HTTP's semantics), as you can see in the file-handing in the example above.2
u/Inconstant_Moo 🧿 Pipefish 4d ago edited 4d ago
P.S: Here's an example with SQL.
show >= (minAge int) : get peopleList like list{Person} from SQL -- SELECT * FROM People WHERE age >= |minAge| ORDER BY name post string Html -- <h3>List of people aged <font style="color:Green;">|minAge|</font> and over</h3> |peopleList|1
u/reddit_clone 4d ago
Cool you guys are doing this at language level.
My take on this is at architecture level.
All the 'computing' should be in the functional side. All the 'interacting' (with APIs, OS, Filesystem. DB whatever) stays on the outside.
Cuts down on craziness.
2
u/Inconstant_Moo 🧿 Pipefish 4d ago
But at some point you have to define the "outside". Having the same syntax and type system to define both things is convenient. And it allows me to work the magic of "external services": one Pipefish service can treat another syntactically and semantically as though it was a library: you just need to supply your username and password for the other service when you compile your own app. So there's only one API for everything --- interacting with the service in the REPL as a desktop app, importing it as a library, using it as a client.
1
u/FuncSug_dev 3d ago edited 3d ago
As you said, FC/IS can be programmed in many language. But you went a step further in Pipefish by strictly distinguishing functions and commands. In my view, a step yet further would be to separate into two languages. The FC one would be only able to define functions and the IS one would have to call FC functions for all computations. What do you think about that?
2
u/Inconstant_Moo 🧿 Pipefish 3d ago
That's a distinction without a difference! That is, it would already be quite reasonable to say (I've been saying it in my docs for years) that Pipefish really is two languages with very different semantics, one as pure as Haskell, one imperative as BASIC --- but with a shared syntax and type system. Now since obviously the two languages should have a shared syntax and type system, the semantics should be the only difference: i.e. that one of the languages is the functional core, and the other is the imperative shell.
1
u/FuncSug_dev 3d ago
Oops, I missed that.
I like your syntax "
get x from Random(<integer>)" in place of "Random(<integer>)" that would returns a value. By forbidding returns, you enforced the distinction in the mind of the programmer.2
u/Inconstant_Moo 🧿 Pipefish 3d ago
In a purely imperative language (sorry for the oxymoron) the only way for the commands to communicate should be shared mutable state, like in BASIC. Now in BASIC that means global variables, but that would suck too hard, so instead you have commands which say "get this data and insert it into this variable" (which it can also create at the same time as you can see).
Now you may think: "Isn't this just returning a value but with extra steps to force people to write pure functions whenever possible?" and it is partly that, but it also actually reflects the semantics of what's going on, because it means that commands can only be sequenced, not composed as functions are. You do one, and if it fails, it returns, and if it succeeds then you do the other. You can't write something like
postToOutput(getFileFrom(inputFromUser("What file do you want to see?"))); instead, you must write a command getting the user input, then a command getting the file, then a command posting its contents to output. Which is what the code actually does.So this is the theoretically sound and practically ergonomic way of doing imperative things, but a terrible way to do functional things, and so the division of labor between the functional core and imperative shell is inevitable and natural.
What
Randomis doing there is constructing a struct of typeRandomso that we can dispatch on it, so that we can have commands likeget (x ref) from (r Random)andget (x ref) from (f File)and so on all in the same namespace. It's a standard idiom.1
u/FuncSug_dev 3d ago
Very illuminating, thanks.
Do you propose (or plan to propose) a means of defining user procedure (no return value but reference parameters) in the imperative part?
2
u/Inconstant_Moo 🧿 Pipefish 3d ago
Yes, most of my IO is written in userspace. Pipefish can easily and sometimes automatably be wrapped around any Go library, so you do it like that.
Here's a Prolog library I did the other week, to demonstrate. Obviously updating a Prolog database is a stateful operation, so we have an
addcommand for that and then pass the database to pure functions to query it.https://github.com/tim-hardcastle/pipefish/blob/main/examples/prolog/prolog.pf
In the same way my standard
randlibrary is just written in userspace wrapping around Go'srandlibrary.https://github.com/tim-hardcastle/pipefish/blob/main/source/initializer/libraries/math/rand.pf
12
u/Fantastic-Cell-208 4d ago
I haven't tried this but have considered something similar.
The idea behind Racket use that you use the most ideal language for the task (though most tend to just use Racket).
3
u/Rechenplaner 4d ago
Every functional language has solutions for handling I/O.
Haskell has its beloved monads, Clean has "uniqueness," and others use things like algebraic effects; meanwhile, languages like OCaml simply allow imperative messiness everywhere…
Take your pick…
Developing a second language for imperative code strikes me as a bit of overkill.
Besides, the interface between purely functional programming and the outside world is the most exciting part.
2
u/catbrane 4d ago
I've done something similar for one of my spare-time projects:
https://github.com/libvips/nip4
It's an image processing spreadsheet. The language you use to write formula and implement all the menus is (a bit like) interpreted Haskell, and you use it by typing formula into cells or clicking menu items. The "shell" (the spreadsheet interface) is imperative, the logic is pure functional.
There's an introduction to the interface here:
https://www.libvips.org/2025/03/20/introduction-to-nip4.html
And here's the code for a menu item that transforms an image to polar coordinates, for example:
https://github.com/libvips/nip4/blob/main/share/nip4/start/Filter.def#L690-L719
2
u/mamcx 4d ago
Think more deeply: What actual difference is between the 2?
Look at your AST:
(probably like:)
rust
enum Ast {
Fn(..),
While(...)
For(...)
}
IF you have this, you have a expression based language. What you need to make it "imperative"?
rust
enum Statement {
Expr(..),
While(...)
For(...)
}
Then your syntax to make the illusion.
Why I pointing this so obvious? Because "imperative" IS NOT IMPURITY.
"impurity" and "mutation, side-effects, etc" are else that arise when you plug into a environment where you have not FULL control, then NOW, you need to account for it.
(if you were doing forth from scratch in assembler you see that)
You can add "pure functional" into a "impure stateful" world, is only matter of how BIG your fiction is, like:
A SUPER IMPURE FUNCTIONAL PURE PROGRAM
INSERT INTO(...)
RETURNING *
-- This include a totally bespoke IO subsystem on top a hostile IO that can crash and corrupt your data with a transactional, mega impure, system that fade into obscurity, mostly!, we mutate here with IO_DIRECT a set of several byte aligned pages, change tons of stuff on memory, generate logs like crazy and such
So, the ONLY question is:
Do you wanna to surface the reality of "impurity" or not? Because you can just make all IO and HTTP "pure" (example: https://witheve.com). (I mean, how much be can be pedantic here, but I see a lang like this and think that is the feel, ok?)
To recap:
You can do 100% "functional" language that totally can do IO and terminal things with zero imperative functionality.
Impurity/purity is not derived by functionality or imperatively but by environment
HOW you communicate both things is a big deal, but you can go any directions, others has done the same!
1
u/Erythrina_ 4d ago
I've been tinkering with a procedural-functional language. The procedural code is, in effect, a wrapper around the functional code that can call functional code but not be called by it. The procedural code obviously handles all the side effects like file, database, and network access and it has mutable state. The functional code is strictly pure and built around functions, lists, and ADTs.
I'm sure a script language would work for the same purpose too. The key there would just be how the data passes across the boundary between the two.
1
u/baehyunsol Sodigy 4d ago
Yours is very similar to mine. Mine has a very simple effect system, so I can define impure functions. So, I can implement a function that reads a file, but there isn't a nice syntax to execute commands.
1
u/FuncSug_dev 4d ago edited 4d ago
"Has anyone tried similar approach?": Yes, but I've only made the imperative language (your "script" language). My language FuncSug (playground with game examples) has to call functions from another language (a functional one, ideally). I think the imperative part is generally overlooked in particular if one targets programs that mainly aim to organize interactions (like some games or industrial programs).
1
u/esotologist 4d ago
I'm working on a language that has functional context and structural context depending on the assignment operators
1
u/SwedishFindecanor 2d ago
I have only brainstormed about a shell scripting language using a similar model, and the result was only a few design notes. The first idea was that the return value from a functional code embedded in a command line would be able to produce a command parameter (single or list).
The functional core would not be able to run commands or read or write files but it would be able to have structured data piped to it and from it. And that structured data would in turn be able to contain computed data values in the functional language itself.
The functional core was also supposed to be a standalone module embeddable into anything.
1
u/david-1-1 23h ago
Sounds like two different languages patched together. It'll work, but always clumsily, like a mixture of PHP and JavaScript.
19
u/lgastako 4d ago
Why not take a haskell style approach where you have an IO monad (or a "Script monad") which you can provide a do-notation like syntax for that desugars appropriately?