How to conditional `require`

The purpose is I want to build a set of low-level LLVM bindings like

  • llvm 12
  • llvm 13
  • llvm 14

And provides a high-level package to choose them dynamically(by detecting LLVM version on the system), does that possible with raco info.rkt?

In general dynamic-require is the way to do a conditional require.

If you're doing FFI bindings to LLVM then I think you could use the version argument to ffi-lib?

You can supply a list of versions, assuming you'll get the same bindings from all versions.

Otherwise I think you could make multiple calls to ffi-lib, one version at a time, until one succeeds. Then you know which version and can supply certain bindings or not?

Also, here's an example from db-lib of calling it differently based on operating system.

Caveat: It's been some years since I worked with FFI, and even then it wasn't sophisticated. Other people will probably have better advice!

3 Likes

Note that dynamic-require still obeys lexical scope so code outside of that scope won't have access to imported bindings. In order to do something like a conditional dynamic-require, you will probably need to return the bindings in a struct or something (assuming you have multiple elements being dynamically imported).

(struct foobar (foo bar))
(define (load-foobar)
  (if some-cond
      (let ([foo (dynamic-require 'foobar-impl1 'foo)]
            [bar (dynamic-require 'foobar-impl1 'bar)])
        (foobar foo bar))
      (let ([foo (dynamic-require 'foobar-impl2 'foo)]
            [bar (dynamic-require 'foobar-impl2 'bar)])
        (foobar foo bar))))

Alternatively, you could have dummy implementations for everything (define foollvm (lambda args (error 'foollvm "function not loaded))) and then later on call (set! foollvm foollvm-impl). As long as the set! is done in the same module as define, this should always work. If going this route, I recommend raising an exception if the dummy function is called before initialized rather than using undefined or #f so that errors are clear and the code fails fast.

(define foo (lambda args (error 'foo "function not loaded")))
(define bar (lambda args (error 'bar "function not loaded")))
(if some-cond
    (let ()
      (set! foo (dynamic-require 'foobar-impl1 'foo))
      (set! bar (dynamic-require 'foobar-impl1 'bar)))
    (let ()
      (set! foo (dynamic-require 'foobar-impl2 'foo))
      (set! bar (dynamic-require 'foobar-impl2 'bar))))
1 Like

I agree it's a good idea to think about the scenario where nothing is found.

Note that dynamic-require still obeys lexical scope so code outside of that scope won't have access to imported bindings. In order to do something like a conditional dynamic-require , you will probably need to return the bindings in a struct or something (assuming you have multiple elements being dynamically imported).

I wouldn't say this is really about lexical scope? dynamic-require isn't a macro, it's just a function that returns a value. (Usually the value is a procedure value: some procedure from that module.) You can bind that returned value to a variable in all the usual ways.

For example you can just:

(define foo (dynamic-require 'modpath 'foo))

as well as do things like:

(define mods-to-try (list 'mod-path-1 'mod-path-2))

(define foo (or (for/or ([mod (in-list mods-to-try)])
                  (with-handlers ([exn:fail? (λ _ #f)])
                    (dynamic-require mod 'foo)))
                (λ _ (error 'foo "no implementation found"))))
2 Likes