Zulip Chat Archive

Stream: new members

Topic: Program Main without IO


Simon Daniel (Aug 28 2024 at 21:43):

Hi,
im writing a library where i want to prevent Users from performing IO in programs, except through my own API. My idea was to provide a 'custom' main method which gets called from within an actual main that is located in the library. Is there some way of forward declaring a function, or supply this class instance in an other file after the main?

--library
class CustomMain where
  main:(List String) -> UInt32 -- no IO here

def main (args : List String) [CustomMain]: IO UInt32 := do --no valid main without CustomMain instance
  return CustomMain.main args

--other file
instance: CustomMain where
  main args := 0

Niels Voss (Aug 30 2024 at 16:04):

Unfortunately no, there is no way to forward declare functions. This is because forward declarations would allow you to use mutual recursion, which would in turn let you prove False.

That being said, maybe there's another way to do it using more advanced features in Lean, like mutual or implementedBy, or even defining a custom command with metaprogramming, but I am not aware of them. You would need to ask someone else.

Do you trust the user of your library not to run code they shouldn't? Because what you could do is make a function def run [CustomMain] : IO UInt32 and then expect users to write def main := run somewhere after declaring an instance of CustomMain

Niels Voss (Aug 30 2024 at 16:09):

You could even in theory make a command with metaprogramming so that you could use run_main (args : List String) : UInit32 := 0 without having to worry about interacting with main directly, though ofc a user that deliberately wants to run impure programs could.

Simon Daniel (Aug 30 2024 at 16:36):

im currently using a run function like you suggested. I guess having to write outdef main := run is just a minor inconvenience.
It would be cool however to prevent the user from running impure code, instead of having to rely on him correctly calling into the library.
A metaprogramming solution also seems nice to hide the main from the user!

Niels Voss (Aug 30 2024 at 18:28):

The problem is that there's no way to tell a file A to do something from a file B unless A imports B. If your library code is in file A, then making B.lean can't really affect A, because A basically has no idea that B.lean even exists. At most, maybe you could do some trick to try to search folders for files like B.lean and dynamically resolve imports, but that's probably more trouble than it's worth.

I do not know if it is possible to attain "inversion of control" where the library contains main and you write a plugin to hook onto that. Maybe you could achieve it by having your end user specify an "entrypoint" in the lakefile? This does sound like more trouble than it's worth

Eric Wieser (Aug 30 2024 at 19:28):

You could write a linter in your library that rejects any definition of main other than def main := run

Eric Wieser (Aug 30 2024 at 19:28):

(linters get to run every time downstream code adds a definition)

Simon Daniel (Aug 31 2024 at 07:40):

seems like linters could do the job, from looking at examples from batteries it should be a Std.Tactic.Linter?

@[env_linter] def checkMain : Std.Tactic.Lint.Linter

with env_linter it seems to only run the linter on a #lint command, is that correct?


Last updated: May 02 2025 at 03:31 UTC