Elixir: dynamic parameter in use statement - module

How could I pass adapter parameter dynamically to Nebulex.Cache module initialization?
defmodule MyApp.RedisCache do
use Nebulex.Cache,
otp_app: :nebulex,
adapter: if true do NebulexRedisAdapter else Nebulex.Adapters.Nil end
end

The easiest way would be to move the conditional one level up.
defmodule MyApp.RedisCache do
if true do
use Nebulex.Cache,
otp_app: :nebulex,
adapter: NebulexRedisAdapter
else
use Nebulex.Cache,
otp_app: :nebulex,
adapter: Nebulex.Adapters.Nil
end
...
end
Another way would be to create a private macro with a parameter, or, alternatively, create a module attribute storing the adapter and pass it to use (assuming Nebulex.Cache does Macro.expand/2 arguments in the __CALLER__ context.)
The reason is that the macro receives the AST, so what will __using__/1 receive, is quote do: if .... which won’t be even attempted to compile at the first compilation stage.
The below code might shed a light on how the AST is passed between calls and what Macro.expand/2 does.
defmodule M do
#param if(true, do: M1, else: M2)
defmacrop t1(arg: args) do
IO.inspect(args, label: "#1")
end
defmacrop t2(arg: args) do
IO.inspect(Macro.expand(args, __CALLER__), label: "#2")
end
def test do
t1(arg: if(true, do: M1, else: M2))
t2(arg: if(true, do: M1, else: M2))
t1(arg: #param)
t2(arg: #param)
end
end
It results in:
#1: {:if, [line: 19],
...
#2: {:case, [optimize_boolean: true],
...
#1: {:#, [line: 21], [{:param, [line: 21], nil}]}
#2: M1
Note, that if was expanded to case, and the module attribute was not expanded unless the explicit Macro.expand/2 was called on it.

Related

Sorbet: how do you add a signature for a dynamically generated method?

I'm using https://github.com/kenn/active_flag and https://github.com/chanzuckerberg/sorbet-rails
This is what its rbi looks like:
module ActiveFlag
extend ActiveSupport::Concern
end
class ActiveFlag::Definition
def column; end
def human(key, options = nil); end
def humans; end
def initialize(column, keys, klass); end
def keys; end
def maps; end
def pairs; end
def set_all!(key); end
def to_array(integer); end
def to_i(arg); end
def to_value(instance, integer); end
def unset_all!(key); end
end
class ActiveFlag::Railtie < Rails::Railtie
end
class ActiveFlag::Value < Set
def method_missing(symbol, *args, &block); end
def raw; end
def set!(key, options = nil); end
def set(key); end
def set?(key); end
def to_human; end
def to_s; end
def unset!(key, options = nil); end
def unset(key); end
def unset?(key); end
def with(instance, definition); end
end
module ActiveFlag::ClassMethods
def flag(column, keys); end
end
class ActiveRecord::Base
extend ActiveFlag::ClassMethods
end
The last bit (extending AR::Base) I added, the rest srb rbi gems generated automatically.
To actually use Active Flag, I then do this in my model:
flag :visible_to, [:employee, :manager, :admin]
visible_to is an integer column. Sorbet Rails has already typed it as such:
sig { returns(Integer) }
def visible_to; end
sig { params(value: Integer).void }
def visible_to=(value); end
sig { returns(T::Boolean) }
def visible_to?; end
I could change this definition, but it's an autogenerated file and I assume the changes will get lost. So the next thing I tried was adding a sig directly above where the method gets used:
sig { returns(ActiveFlag::Value) }
def visible_to; end
flag :visible_to, [:employee, :manager, :admin]
The problem here is that Sorbet fails because the method I've "defined" doesn't return anything. Which I know is fine, because it'll get overriden when flag is called (it uses define_method internally), but I don't know how to communicate this to Sorbet.
Returning value that does not conform to method result type https://srb.help/7005
54 | def visible_to; end
^^^^^^^^^^^^^^^^^^^
Expected ActiveFlag::Value
Method visible_to has return type ActiveFlag::Value
54 | def visible_to; end
^^^^^^^^^^^^^^
Got NilClass originating from:
54 | def visible_to; e
So my question is. What's the best way to tell Sorbet that this method will return an ActiveFlag::Value once it gets defined, ideally without making a manual change to an autogenerated file?
btw. I tried to see how types for enum in Rails are handled... it doesn't look like that's been done on sorbet-typed yet. Possibly for the same reason.
When you want to override a generated rbi file you can create a second rbi file for the same class in your workspace and move the methods in there. Sorbet will handle merging multiple definitions files for you. We keep these files in a separate sorbet/custom directory next to the generated files; others keep the rbi files next to the rb files in their application directory.
As to enums, Sorbet does not natively support enum literal types so that's likely why.
You could implement a custom plugin for sorbet-rails to generate methods added by active_flag and remove the sigs generated wrongly. Here is the documentation for it:
https://github.com/chanzuckerberg/sorbet-rails/blob/master/README.md#extending-model-generation-task-with-custom-plugins

Why not infer T::Boolean for true and false?

Sorbet infers the type of true to be TrueClass, and the type of false to be FalseClass. Often it would be nice if it would instead infer T::Boolean. Why not special case true and false to have the type T::Boolean instead?
It's possible to work around this problem with a type annotation, initializing variables with T.let(true, T::Boolean) for example, but it would be nice to not have to provide this extra information.
# typed: true
T.reveal_type(true) # Revealed type: `TrueClass`
T.reveal_type(false) # Revealed type: `FalseClass`
extend T::Sig
sig {params(x: T::Boolean).void}
def test(x)
var = true
10.times do
var = false # Changing the type of a variable in a loop is not permitted
end
end
The assignment of false to var in the loop causes an error to be raised, as the type of var is being changed from TrueClass to FalseClass.
Sorbet's flow-sensitive typing is made more precise by true and false having different types. In the following example, a variable with the value true is used as the condition of an if-statement:
# typed: true
val = true
if val
puts "true!"
else
puts "false?"
end
The resulting error from sorbet is:
editor.rb:7: This code is unreachable https://srb.help/7006
7 | puts "false?"
^^^^^^^^
Errors: 1
Behind the scenes, sorbet knows that the value being examined has the type TrueClass, and that the value true is the only value of that type. As a result, it knows that val cannot be false, and that the else branch will never be executed.
Now consider the case where we instead infer the type T::Boolean for true and false. T::Boolean is a synonym for T.any(TrueClass, FalseClass), so in the example it now means that val could be either true or false. As a result, it becomes impossible to tell from the type alone that the else branch will not be executed.
The flow-sensitive typing documentation on sorbet.org has more information on this topic.
Asked the same question today as well.
Ended up fixing it as you suggested with type annotations and initializing with T.let(true, T::Boolean)
# typed: true
extend T::Sig
sig {params(x: T::Boolean).void}
def test(x)
var = T.let(true, T::Boolean)
10.times do
var = T.let(false, T::Boolean)
end
end

How to test an infinite, recursive Task in Elixir

Please check this code:
defmodule InfinitePollTask do
def poll(opts \\ [])
# function body code here
poll(new_opts)
end
end
I want to write a unit test for the function body code, assuming the function body perform some important computation using opts and produce a new_opts for the next iteration.
I'd just pull the computation out into a separate function that returns new_opts, and test that:
defmodule InfinitePollTask do
def poll(opts \\ [])
poll(do_poll(opts))
end
def do_poll(opts)
# important computation
end
end
defmodule InfinitePollTaskTest do
use ExUnit.Case
test "some case" do
assert InfinitePollTask.do_poll(some_opts) == some_result_opts
end
end

Calling functions on require()d modules in Lua gives me "attempt to index local 'x' (a boolean value)"

I've read PIL and the ModulesTutorial on creating modules but I'm having trouble require()ing them correctly.
Here is my setup:
-- File ./lib/3rdparty/set.lua
local ipairs = ipairs
module( "set" )
function newSet (t)
local set = {}
for _, l in ipairs(t) do set[l] = true end
return set
end
And:
-- File ./snowplow.lua
local set = require( "lib.3rdparty.set" )
module( "snowplow" )
local SUPPORTED_PLATFORMS = set.newSet { "pc", "tv", "mob", "con", "iot" }
Then if I run snowplow.lua:
lua: snowplow.lua:4: attempt to index local 'set' (a boolean value)
stack traceback:
snowplow.lua:4: in main chunk
[C]: ?
What am I doing wrong in my module definition - what is the boolean exactly? Also, if I append a return _M; at the bottom of my set.lua, then everything starts working - why?
true is usually returned by require if module function is not used inside module and module code doesn't return a value.
But anyway it seems strange.
-- file m0.lua
module'm0'
--file dir1\m1.lua
module'm1'
--file test.lua
print(require'm0')
print(m0)
print(require'dir1.m1')
print(m1)
for k,v in pairs(package.loaded) do
if k:match'm%d' then print(k, v) end
end
--output
table: 0036C8C8
table: 0036C8C8
true
table: 0036B6B0
m0 table: 0036C8C8
m1 table: 0036B6B0
dir1.m1 true
So, you can simply use global variable set instead of local set assigned a value returned by require.
UPD :
It is recommended to avoid using module function and always return your table at the end of your module. In that case the whole picture is just fine:
-- file m0.lua
return 'string0'
--file dir1\m1.lua
return 'string1'
--file test.lua
print(require'm0')
print(m0)
print(require'dir1.m1')
print(m1)
for k,v in pairs(package.loaded) do
if k:match'm%d' then print(k, v) end
end
--output
string0
nil
string1
nil
m0 string0
dir1.m1 string1
UPD2 :
Problem disappears if you replace module( "set" ) with module('lib.3rdparty.set').
So, each module must remember its relative path.
Now you could access it either by calling require'lib.3rdparty.set' or by reading global variable lib.3rdparty.set - the result would be the same.
require("lib.moduleName")
local moduleName = moduleName
I'm not sure why Lua returns a boolean when you require a module on a different directory, but it seems the module is correctly set on the the global variable. So I simply used that and put it on a local variable with the same name.

Is there an existing I18N translation for booleans?

I need to display "Yes" or "No" in various languages based on whether a expression is true or false. Currently I am doing it like this:
fr.yml:
fr:
"yes": Oui
"no": Non
a helper method:
def t_boolean(expression)
(expression) ? t("yes") : t("no")
end
erb:
Valid: <%= t_boolean(something.is_valid?) %>
Is there some better way to do this?
Does Rails already have translations for true/false like this?
After reading this, I got inspired and figured out this solution:
fr.yml
fr:
"true": Oui
"false": Non
erb:
Valid: <%= t something.is_valid?.to_s %>
Update
For english, if you want to use yes and no as values, be sure to quote them:
en.yml
en:
"true": "yes"
"false": "no"
Just as Zabba says works fine, but if you are trying to translate true-false into yes-no, quote both sides, else you'll get true translated into true (TrueClass) again.
en:
"true": "yes"
"false": "no"
You may try overriding I18n's default translate method, delegating to the default method to do the actual translation. Use this code in an initializer:
module I18n
class << self
alias :__translate :translate # move the current self.translate() to self.__translate()
def translate(key, options = {})
if key.class == TrueClass || key.class == FalseClass
return key ? self.__translate("yes", options) : self.__translate("no", options)
else
return self.__translate(key, options)
end
end
end
end
# Inside initializer
module I18n
class << self
alias :__translate :translate # move the current self.translate() to self.__translate()
alias :t :translate
def translate(key, options = {})
if key.class == TrueClass || key.class == FalseClass
return key ? self.__translate("boolean.true", options) : self.__translate("boolean.false", options)
else
return self.__translate(key, options)
end
end
end
end
# Inside locale
boolean:
:true: 'Yes'
:false: 'No'
# Calling translate
I18n.translate(is_this_my_boolean_column)
Working with Rails 3.2.2 :)
Remember that the translate method had been aliased in I18n.
When you alias a method you are actually creating a new copy of it so only redefining the translate method will not work when calls to the t method occurs.
In order to make the above code to work you could, for example, alias the t method, too.
module I18n
class << self
alias :__translate :translate # move the current self.translate() to self.__translate()
alias :t : translate # move the current self.t() to self.translate()
def translate(key, options = {})
if key.class == TrueClass || key.class == FalseClass
return key ? self.__translate("yes", options) : self.__translate("no", options)
else
return self.__translate(key, options)
end
end
end
end
Other solution I prefer:
# Create a helper
def yes_no(bool_value)
if bool_value
t(:yes_word)
else
t(:no_word)
end
end
# Add the translations, important that you use " around yes or no.
yes_word: "No"
no_word: "Yes"
# In your views, in my case in slim:
span= yes_no myvalue
# Or ERB
<%= yes_no(myvalue) %>
For any boolean translation
I just love that boolean pluralization hack
# some_view.html.erb
t(:are_you_ok?, count: (user.is_ok? ? 0 : 1) ).html_safe
Translations
# locales/en.yml
en:
are_you_ok?:
zero: "You are <strong>NOT</strong> ok ! Do something !"
one: "You are doing fine"
You don't even need quotes actually ^^.