I've wasted more time than I'd like to admit trying to:
Fabricate a simple macro or template to reduce the tedious boilerplate code required to setup up lazy ( deferred i.e. only define on first access) object fields.
My stumbling block is extrapolating untype parameter data into valid Nim accepted syntax within the template/macro.
eg. user -> m_user in the code below.
I've tried various combinations of templates / macro/ quote do code to no avail :-(.
I'm sure this is quite trivial in the hands of a more experienced Nim meta-programmer. Any help would be surely appreciated.
import std/envvars, std/options
type
EnvType = object
m_user: Option[string]
var Env*: EnvType
# How can I reduce boilerplate for defining deferred memoized object data field accessors
# (eg. user)
# using macro / template for the following
# with parameters for:
# - name - untyped fieldname -- eg. m_user
# - value = untyped code to computer actual value -- eg "USER".getEnv
#
# Ideally I'd like a macro/template define_env:
#
# define_env user do:
# USER.getEnv
#
# to generate:
proc user*(env: var EnvType): string =
if isNone(env.m_user):
result = "USER".getEnv
env.m_user = some result
else:
result = env.m_user.get
when isMainModule:
# define_env user do: USER.getenv
# define_env icloud_dir do: "/Users/" & env.USER & "/Library/CloudStorage/Box-Box"
echo Env.user
# echo Env.icloud_dir
I'm not 100% sure if I understand you correctly. This is what seems closest to what I understand you have in mind. Note the comments I added:
import std/envvars, std/options
type
EnvType = object
m_user: Option[string]
m_icloud_dir: Option[string]
var Env*: EnvType
## - `prcname` is the name of the produced helper procedure
## - `name` is the field name of the `EnvType`
## - `body` is the code inserted for the `isNone` `result = body`
## Note:
## - assumes all types are of field `Option[string]` (one could either pass a
## type or make it much more complicated by trying to determine the type from
## the `EnvType` (but that is problematic, because it would need to be a `typed` macro
## which clashes with using free identifiers)
## - the `{.inject.}` is used to make the `env` available in the body of the template
## - I switched away from a `do` approach and instead just use a block body
template define_env(prcname, name, body: untyped): untyped =
proc `prcname`*(env {.inject.}: var EnvType): string =
if isNone(env.name):
result = body
env.name = some result
else:
result = env.name.get
define_env(user, m_user): "USER".getenv
define_env(icloud_dir, m_icloud_dir): "/Users/" & env.user & "/Library/CloudStorage/Box-Box"
when isMainModule:
echo Env.user
echo Env.icloud_dir
I think this is exactly what you want, minus the do:
import macros
import std/envvars, std/options
type
EnvType = object
m_user: Option[string]
m_icloud_dir: Option[string]
var Env*: EnvType
macro defineEnv*(fieldName, body: untyped): untyped =
let newName = ident("m_" & fieldName.strVal)
return quote do:
proc `fieldName`*(env: var EnvType): string =
if isNone(env.`newName`):
result = `body`
env.`newName` = some result
else:
result = env.`newName`.get
define_env user:
"USER".getEnv
define_env icloud_dir: "/Users/" & Env.user & "/Library/CloudStorage/Box-Box"
echo Env.user
echo Env.icloud_dir
You can also go crazy and get rid of even more boiler plate (I've added types as well):
# env.nim
import macros, tables
import std/envvars, std/options
export envvars, options
type
EnvVarInfo = tuple[envType: string, envBody: NimNode]
# Store all of the compile time info
var envTable {.compileTime.}: TableRef[string, EnvVarInfo] = newTable[string, EnvVarInfo]()
macro defineEnv*(fieldName, fieldType, body: untyped): untyped =
# create the new field name
let newName = ident("m_" & fieldName.strVal)
# body of our code
let body = quote do:
# Use fieldName and fieldType as per needed.
# Also, inject Env into the body code so that it can be
# referenced inside the body of the env code with the envs
# that were already declared
proc `fieldName`*(Env {.inject.}: var EnvType): `fieldType` =
if isNone(Env.`newName`):
result = `body`
Env.`newName` = some result
else:
result = Env.`newName`.get
envTable[newName.strVal] = (fieldType.strVal, body)
proc createEnvTypeField(envName: string, envType: string): NimNode =
result = nnkIdentDefs.newTree(
ident(envName),
nnkBracketExpr.newTree(
ident("Option"),
ident(envType)
),
newEmptyNode()
)
macro buildEnvType*(): untyped =
# build the env type and all of the procs and the var declaration
let recList = nnkRecList.newNimNode()
result = nnkStmtList.newTree()
let envTy = nnkTypeSection.newTree(
nnkTypeDef.newTree(
ident("EnvType"),
newEmptyNode(),
nnkObjectTy.newTree(
newEmptyNode(),
newEmptyNode(),
recList
)
)
)
# add the type before the procs
result.add(envTy)
for envName, env in envTable.pairs:
recList.add(createEnvTypeField(envName, env.envType))
result.add(env.envBody)
# This is needed so that nim makes the var name
# exactly as spelled and doesn't add any hash values
# to the name. This makes it able to be referenced outside of
# the macro
let envVarName = ident("Env")
result.add quote do:
var `envVarName`*: EnvType
Usage:
# yourfile.nim
import env
defineEnv user, string:
"USER".getEnv
defineEnv icloud_dir, string:
"/Users/" & Env.user & "/Library/CloudStorage/Box-Box"
defineEnv num_things, int:
42
# Must call this AFTER all defineEnv calls
buildEnvType()
echo Env.user
echo Env.icloud_dir
echo Env.num_things
Thanks @Vindaar and @jyapayne... I can't keep up! 🙂
I was still mapping @Vindaars suggestion into a more general Lazy template and @jyapayne anticipated my desire to reduce the redundant boilerplate even further!
I will try to add his latest refinements to what I currently have.
# Lazy object member accessor support
template lazy*(
obj, ObjectType,
accessorName,
m_name, m_nameType,
body: untyped): untyped =
proc accessorName*(obj {.inject.}: var ObjectType): m_nameType =
if isNone obj.m_name:
result = body
obj.m_name = some result
else:
result = obj.m_name.get
# Sample usage of lazy access to user defined enviroment
import options, std/envvars
type
Env = object
m_user: Option[string]
m_box_sync_dir: Option[string]
m_downloads_dir: Option[string]
var env*: Env
lazy env, Env, user, m_user, string,
"USER".getEnv
lazy env, Env, box_sync_dir, m_box_sync_dir, string,
"/Users/" & env.user & "/Library/CloudStorage/Box-Box"
lazy env, Env, downloads_dir, m_downloads_dir, string,
"/Users/" & env.user & "/Downloads"
# Show that it actually works
when isMainModule:
echo "user : ", env.user
echo "box_sync_dir : ", env.box_sync_dir
echo "downloads_dir : ", env.downloads_dir
I will post an updated version for your comments.
Thanks again for your valued assistance.