I'm trying write a test that verifies that a behaviour defines the callbacks it's supposed to. How should I do this?
I have a module that defines a callback, for example:
defmodule MyModule do
#callback my_callback(arg :: binary) :: any
end
I want to ensure that my_callback/1 is defined by MyModule.
Since #callback is an attribute, I tried calling MyModule.__info__(:attributes), but the callback was not present in the response.
Although it wasn't documented except in a deprecated module at the time the question was asked, this is now documented in Typespecs:
Inspecting behaviours
The #callback and #optional_callback attributes are used to create
a behaviour_info/1 function available on the defining module. This
function can be used to retrieve the callbacks and optional callbacks
defined by that module.
For example, for the MyBehaviour module defined in "Optional
callbacks" above:
MyBehaviour.behaviour_info(:callbacks)
#=> [vital_fun: 0, "MACRO-non_vital_macro": 2, non_vital_fun: 0]
MyBehaviour.behaviour_info(:optional_callbacks)
#=> ["MACRO-non_vital_macro": 2, non_vital_fun: 0]
When using iex, the IEx.Helpers.b/1 helper is also available.
MyModule.behaviour_info(:callbacks).
For unknown reason it’s mentioned only in docs for deprecated Behaviour module.
Beware that this function is exported if and only the module does indeed define a behaviour.
Integer.behaviour_info(:callbacks)
** (UndefinedFunctionError) function Integer.behaviour_info/1
is undefined or private
Fancy discovery: one might also define the behaviour manually.
I'm using graphql-ruby, and I'd really like to be able to type the dynamic methods that created for things like arguments.
Small example:
class Test
argument :first_argument, String
argument :secondArgument, String, as: second_argument, required: false
def method
puts first_argument.length # this is okay
puts second_argument.length # this is a problem, because it can be nil
end
end
I've tried to define these by doing:
# ...
first_argument = T.let(nil, String)
second_argument = T.let(nil, T.nilable(String))
which doesn't seem to work. I also did
#...
sig { returns(String) }
def first_argument; ""; end
sig { returns(T.nilable(String)) }
def second_argument; end
which works, but is not overly pretty. Is there a nicer way to go about this?
There is some nascent, experimental support for typing methods declared by meta programming like this: https://sorbet.org/docs/metaprogramming-plugins
In this case, you might define a plugin file like:
# argument_plugin.rb
# Sorbet calls this plugin with command line arguments similar to the following:
# ruby --class Test --method argument --source "argument :first_argument, String"
# we only care about the source here, so we use ARGV[5]
source = ARGV[5]
/argument[( ]:([^,]*?), ([^,]*?)[) ]/.match(source) do |match_data|
puts "sig {return(#{match_data[2]})}" # writes a sig that returns the type
puts "def #{match_data[1]}; end" # writes an empty method with the right name
end
I've only included the "getter" for the argument here, but it should be simple to go ahead and write out the sig for the setter method as well. You'd also want to handle all variants of the argument method as I've only handled the one with Symbol, Type arguments. For what it's worth, I'm not sure if the "source" passed in to your plugin would be normalized with parens or not, so I've made the regex match either. I also suspect that this will not work if you pass in the symbol names as variables instead of literals.
We then use a YAML file to tell Sorbet about this plugin.
# triggers.yaml
ruby_extra_args:
# These options are forwarded to Ruby
- '--disable-gems' # This option speeds up Ruby boot time. Use it if you don't need gems
triggers:
argument: argument_plugin.rb # This tells Sorbet to run argument.rb when it sees a call to `argument`
Run Sorbet and pass in the yaml config file as the argument for --dsl-plugins:
❯ srb tc --dsl-plugins triggers.yaml ... files to type check ...
I'd really like to be able to type the dynamic methods that created for things like arguments
Sorbet doesn't support typing dynamic methods like that. But they do provide a T::Struct class that has similar functionality. I did something similar last week for my project and I'll describe what I did below. If T::Struct doesn't work for you, an alternative is writing some code to generate the Sigs that you'd write manually.
My approach is using T::Struct as the wrapper for “arguments” class. You can define args as props in a T::Struct like following:
const for arguments that don’t change
prop for arguments that may change
Use default to provide a default value when no value is given
Use T.nilable type for arguments that can be nil
Building on top of the vanilla T::Struct, I also add support for “maybe”, which is for args that is truly optional and can be nil. IE: when a value is not passed in, it should not be used at all. It is different from using nil as default value because when a value is passed in, it can be nil. If you’re interested in this “maybe” component, feel free to DM me.
There are two packages I want to use: CorpusLoaders.jl, and WordNet.jl
CorpusLoaders.SemCor exports sensekey(::SenseTaggedWord)
WordNet exports sensekey(::DB, ::Synset, ::Lemma)
I want to use both sensekey methods.
Eg
for some mixed list of items: mixedlist::Vector{Union{Tuple{SenseTaggedWord},Tuple{DB, Synset,Lemma}}.
Ie the items in the list are a mixture of 1-tuples of SenseTaggedWord, and3 tuples of DB, Synset, and Lemma.
for item in mixedlist
println(sensekey(item...)
end
should work.
This example is a little facetious, since why would I be mixing them like this.
But, hopefully it serves for illustrating the problem in the general case.
Trying to using CorpusLoaders.SemCor, WordNet to bring in both results in WARNING: both WordNet and Semcor export "sensekey"; uses of it in module Main must be qualified.
Manually importing both: import CorpusLoaders.SemCor.sensekey; import WordNet.sensekey results in WARNING: ignoring conflicting import of Semcor.sensekey into Main
What can be done? I want them both, and they don't really conflict, due to multiple-dispatch.
Given that CorpusLoaders.jl is a package I am writing I do have a few more options, since I could make my CorpusLoaders.jl depend on WordNet.jl.
If I did do than then I could say in CorpusLoaders.jl
import WordNet
function WordNet.sensekey(s::SenseTaggedWord)...
and that would make them both work.
But it would mean requiring WordNet as a dependency of CorpusLoaders.
And I want to know how to solve the problem for a consumer of the packages -- not as the creator of the packages.
tl;dr qualify the functions when using them in your script via their module namespace, i.e. CorpusLoader.sensekey() and WordNet.sensekey()
Explanation
My understanding of your question after the edits (thank you for clarifying) is that:
You have written a package called CorpusLoaders.jl, which exports the function sensekey(::SenseTaggedWord)
There is an external package called WordNet.jl, which exports the function sensekey(::DB, ::Synset, ::Lemma)
You have a script that makes use of both modules.
and you are worried that using the modules or "importing" the functions directly could potentially create ambiguity and / or errors in your script, asking
how can I write my CorpusLoaders package to prevent potential clashes with other packages, and
how can I write my script to clearly disambiguate between the two functions while still allowing their use?
I think this stems from a slight confusion how using and import are different from each other, and how modules create a namespace. This is very nicely explained in the docs here.
In essence, the answers are:
You should not worry about exporting things from your module that will clash with other modules. This is what modules are for: you're creating a namespace, which will "qualify" all exported variables, e.g. CorpusLoaders.sensekey(::SenseTaggedWord).
When you type using CorpusLoaders, what you're saying to julia is "import the module itself, and all the exported variables stripped from their namespace qualifier, and bring them into Main". Note that this means you now have access to sensekey as a function directly from Main without a namespace qualifier, and as CorpusLoaders.sensekey(), since you've also imported the module as a variable you can use.
If you then try using the module WordNet as well, julia very reasonably issues a warning, which essentially says:
"You've imported two functions that have the same name. I can't just strip their namespace off because that could create problems in some scenarios (even though in your case it wouldn't because they have different signatures, but I couldn't possibly know this in general). If you want to use either of these functions, please do so using their appropriate namespace qualifier".
So, the solution for 2. is:
you either do
using CorpusLoaders;
using WordNet;
, disregarding the warning, to import all other exported variables as usual in your Main namespace, and access those particular functions directly via their modules as CorpusLoaders.sensekey() and WordNet.sensekey() each time you need to use them in your script, or
you keep both modules clearly disambiguated at all times by doing
import CorpusLoaders;
import WordNet;
and qualify all variables appropriately, or
in this particular case where the function signatures don't clash, if you'd really like to be able to use the function without a namespace qualifier, relying on multiple dispatch instead, you can do something like what FengYang suggested:
import CorpusLoaders;
import WordNet;
sensekey(a::SenseTaggedWord) = CorpusLoader.sensekey(a);
sensekey(a::DB, b::Synset, c::Lemma) = WordNet.sensekey(a, b, c);
which is essentially a new function, defined on module Main, acting as a wrapper for the two namespace-qualified functions.
In the end, it all comes down to using using vs import and namespaces appropriately for your particular code. :)
As an addendum, code can get very unwieldy with long namespace qualifiers like CorpusLoader and WordNet. julia doesn't have something like python's import numpy as np, but at the same time modules become simple variables on your workspace, so it's trivial to create an alias for them. So you can do:
import CorpusLoaders; const cl = CorpusLoaders;
import Wordnet; const wn = WordNet;
# ... code using both cl.sensekey() and wn.sensekey()
In this case, the functions do not conflict, but in general that is impossible to guarantee. It could be the case that a package loaded later will add methods to one of the functions that will conflict. So to be able to use the sensekey for both packages requires some additional guarantees and restrictions.
One way to do this is to ignore both package's sensekey, and instead provide your own, dispatching to the correct package:
sensekey(x) = CorpusLoaders.sensekey(x)
sensekey(x, y, z) = WordNet.sensekey(x,y,z)
I implemented what #Fengyang Wang said,
as a function:
function importfrom(moduleinstance::Module, functionname::Symbol, argtypes::Tuple)
meths = methods(moduleinstance.(functionname), argtypes)
importfrom(moduleinstance, functionname, meths)
end
function importfrom(moduleinstance::Module, functionname::Symbol)
meths = methods(moduleinstance.(functionname))
importfrom(moduleinstance, functionname, meths)
end
function importfrom(moduleinstance::Module, functionname::Symbol, meths::Base.MethodList)
for mt in meths
paramnames = collect(mt.lambda_template.slotnames[2:end])
paramtypes = collect(mt.sig.parameters[2:end])
paramsig = ((n,t)->Expr(:(::),n,t)).(paramnames, paramtypes)
funcdec = Expr(:(=),
Expr(:call, functionname, paramsig...),
Expr(:call, :($moduleinstance.$functionname), paramnames...)
)
current_module().eval(funcdec) #Runs at global scope, from calling module
end
end
Call with:
using WordNet
using CorpusLoaders.Semcor
importfrom(CorpusLoaders.Semcor, :sensekey)
importfrom(WordNet, :sensekey)
methods(sensekey)
2 methods for generic function sensekey:
sensekey(db::WordNet.DB, ss::WordNet.Synset, lem::WordNet.Lemma)
sensekey(saword::CorpusLoaders.Semcor.SenseAnnotatedWord
If you wanted to get really flash you could reexport the DocString too.
I have a class Ellipse (handle, inherits from other class), that has one static method called createFromGaussian. It is located in a remote folder, that I add to Matlab path.
The thing is that, if I try to invoke the static function BEFORE creating any Ellipse object, it fails:
>> Ellipse.createFromGaussian(arg1,arg2)
Undefined variable "Ellipse" or class "Ellipse.createFromGaussian".
It works if I try any of the following things:
I change current directory to that in which Ellipse.m file is located
Working from a remote directory, I create an Ellipse object beforehand:
>> Ellipse()
[C=, axis=[0.0,0.0], angle=0.0]
>> Ellipse.createFromGaussian([],2)
Is this supposed to be this way? The error message sounds weird to me: of course it cannot find variable "Ellipse" or class "Ellipse.createFromGaussian"! It should find "Ellipse" class
So the Ellipse classdef file, and the function file, are in the same folder called #Ellipse, and the PARENT of the #Ellipse folder is on the path? That is what Matlab requires for it's system to work.
From the the ML help : "You must use an #-folder if you want to use more than one file for your class definition. Methods defined in separate files match the file name to the function name and must be declared in the classdef file."
I am accessing model Company::Item in controller Security::MyController. It is giving error uninitialised constant Security::Company::Item . So basically it is appending 'Security::' for given model. It is not the case with some other models say Security::User(model in same module security). What could be possible explanation for this?
This is a scope resolution problem. You should try using ::Company::Iteminside Security::MyController
According to Ruby Language Spec
::Something is a shortcut for Object::Something. The idea is that ::Something
should look up a constant called Something in the global scope, but since ruby
doesn't truly have a global scope, it looks it up in the Object class, which is
the nearest thing in ruby to a global scope.
Prefixing :: will prevent Ruby from applying the default scope in that context, which in your case, is the Security:: scope