Skip to content

Commit

Permalink
Support keyword arguments in CallJuliaFunctionWithCatch, and a few …
Browse files Browse the repository at this point in the history
…other improvements (#1043)

- support keyword arguments in `CallJuliaFunctionWithCatch`
- do not use the deprecated and undocumented `Core._apply`
- start better documentation of Julia syntax features
  • Loading branch information
ThomasBreuer authored Sep 24, 2024
1 parent 6d0bbcf commit e5c5358
Show file tree
Hide file tree
Showing 5 changed files with 108 additions and 36 deletions.
63 changes: 51 additions & 12 deletions pkg/JuliaInterface/gap/JuliaInterface.gd
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,24 @@ DeclareGlobalFunction( "JuliaImportPackage" );


#! @Section Access to &Julia; objects
#! Not all &Julia; syntax features are supported in ⪆.
#! For important ones, the interface provides ⪆ functions or
#! helper functions written in &Julia; to use them in ⪆.
#! For example, <Ref Func="CallJuliaFunctionWithCatch"/> allows one to use
#! &Julia;'s try/catch statements.
#!
#! Here is a selection of other workarounds for &Julia; syntax features.
#! <List>
#! <Item>
#! &Julia;'s <C>RefValue</C> objects can be handled as follows.
#! If <C>x</C> is such an object then its value can be fetched with
#! <C>Julia.GAP.getindex( x )</C>,
#! a value <C>v</C> of the right type can be set with
#! <C>Julia.GAP.setindex( x, v )</C>,
#! and one can check with <C>Julia.GAP.isassigned( x )</C>
#! whether <C>x</C> has a value.
#! </Item>
#! </List>

#! @Description
#! This global variable represents the &Julia; module <C>Main</C>,
Expand All @@ -242,6 +260,19 @@ DeclareGlobalFunction( "JuliaImportPackage" );
#! gap> Julia.Main.x;
#! 1
#! @EndExampleSession
#!
#! Note that not all &Julia; variables are directly visible in its
#! <C>Main</C> module.
#! For example, &Julia; variables from the interface to &GAP; are defined
#! in the &Julia; module <C>GAP</C> or its submodules.
#! It is safe to access this module as <C>Julia.GAP</C>.
#!
#! @BeginExampleSession
#! gap> Julia.GAP;
#! <Julia module GAP>
#! gap> Julia.GAP.prompt;
#! <Julia: prompt>
#! @EndExampleSession
DeclareGlobalVariable( "Julia" );

#! @Arguments juliaobj
Expand All @@ -259,11 +290,13 @@ DeclareGlobalVariable( "Julia" );
#! @EndExampleSession
DeclareGlobalFunction( "JuliaTypeInfo" );

#! @Arguments juliafunc, arguments
#! @Arguments juliafunc, arguments[, kwargs]
#! @Returns a record.
#! @Description
#! The function calls the &Julia; function <A>juliafunc</A>
#! with arguments in the &GAP; list <A>arguments</A>,
#! with ordinary arguments in the &GAP; list <A>arguments</A>
#! and optionally with keyword arguments given by the component names (keys)
#! and values of the &GAP; record <A>kwargs</A>,
#! and returns a record with the components <C>ok</C> and <C>value</C>.
#! If no error occurred then <C>ok</C> has the value <K>true</K>,
#! and <C>value</C> is the value returned by <A>juliafunc</A>.
Expand All @@ -286,20 +319,29 @@ DeclareGlobalFunction( "JuliaTypeInfo" );
#! false
#! gap> res.value{ [ 1 .. Position( res.value, '(' )-1 ] };
#! "LinearAlgebra.SingularException"
#! gap> fun:= Julia.range;;
#! gap> CallJuliaFunctionWithCatch( fun, [ 2, 10 ], rec( step:= 2 ) );
#! rec( ok := true, value := <Julia: 2:2:10> )
#! gap> res:= CallJuliaFunctionWithCatch( fun, [ 2, 10 ],
#! > rec( step:= GAPToJulia( "a" ) ) );;
#! gap> res.ok;
#! false
#! gap> res.value{ [ 1 .. Position( res.value, '(' )-1 ] };
#! "MethodError"
#! @EndExampleSession
DeclareGlobalFunction( "CallJuliaFunctionWithCatch" );

#! @Arguments juliafunc, arguments, arec
#! @Arguments juliafunc, arguments, kwargs
#! @Returns the result of the &Julia; function call.
#! @Description
#! The function calls the &Julia; function <A>juliafunc</A>
#! with ordinary arguments in the &GAP; list <A>arguments</A>
#! and keyword arguments given by the component names (keys) and values
#! of the record <A>arec</A>,
#! of the record <A>kwargs</A>,
#! and returns the function value.
#!
#! Note that the entries of <A>arguments</A> and the components of
#! <A>arec</A> are not implicitly converted to &Julia;.
#! <A>kwargs</A> are not implicitly converted to &Julia;.
#! @BeginExampleSession
#! gap> CallJuliaFunctionWithKeywordArguments( Julia.Base.round,
#! > [ GAPToJulia( Float( 1/3 ) ) ], rec( digits:= 5 ) );
Expand Down Expand Up @@ -388,19 +430,17 @@ DeclareGlobalFunction( "CallJuliaFunctionWithKeywordArguments" );
#! <List>
#! <Item>
#! <Ref Oper="CallFuncList" BookName="ref"/>,
#! delegating to <C>Julia.Core._apply</C>
#! (this yields the function call syntax in &GAP;,
#! delegating to &Julia;'s <C>func(args...)</C> syntax;
#! this yields the function call syntax in &GAP;,
#! it is installed also for objects in
#! <Ref Filt="IsJuliaWrapper" Label="for IsObject"/>,
#! </Item>
#! <Item>
#! access to and assignment of entries of arrays, via
#! <Ref Oper="\[\]" BookName="ref"/>,
#! <Ref Oper="\[\]\:\=" BookName="ref"/>,
#! <!-- <Ref Oper="MatElm" BookName="ref"/>, and
#! <Ref Oper="SetMatElm" BookName="ref"/>, -->
#! and the (up to &GAP; 4.11 undocumented) operations <C>MatElm</C> and
#! <C>SetMatElm</C>,
#! <Ref Oper="MatElm" BookName="ref"/>, and
#! <Ref Oper="SetMatElm" BookName="ref"/>,
#! delegating to
#! <C>Julia.Base.getindex</C> and
#! <C>Julia.Base.setindex</C>,
Expand Down Expand Up @@ -459,7 +499,6 @@ DeclareGlobalFunction( "CallJuliaFunctionWithKeywordArguments" );
#! gap> m + m;
#! <Julia: [2 4; 6 8]>
#! @EndExampleSession
#TODO: add the cross-references to MatElm, SetMatElm when they are documented

#! @InsertChunk JuliaHelpInGAP

Expand Down
17 changes: 12 additions & 5 deletions pkg/JuliaInterface/gap/calls.gi
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ InstallMethod( CallFuncList,
[ "IsJuliaObject", "IsList" ],
function( julia_obj, args )
args := GAPToJulia( _JL_Vector_Any, args, false );
return Julia.Core._apply( julia_obj, args );
return Julia.GAP._apply( julia_obj, args );
end );

InstallMethod( CallFuncList,
Expand All @@ -22,14 +22,21 @@ InstallMethod( CallFuncList,
end );

InstallGlobalFunction( CallJuliaFunctionWithCatch,
function( julia_obj, args )
function( julia_obj, args, kwargs... )
local res;

args := GAPToJulia( _JL_Vector_Any, args, false );
if IsFunction( julia_obj ) then
julia_obj:= Julia.GAP.UnwrapJuliaFunc( julia_obj );
fi;
res:= Julia.GAP.call_with_catch( julia_obj, args );
if Length( kwargs ) = 0 then
res:= Julia.GAP.call_with_catch( julia_obj, args );
elif Length( kwargs ) = 1 and IsRecord( kwargs[1] ) then
kwargs := GAPToJulia( _JL_Dict_Any, kwargs[1], false );
res:= Julia.GAP.call_with_catch( julia_obj, args, kwargs );
else
Error( "usage: CallJuliaFunctionWithCatch( <julia_obj>, <args>[, <kwargs>]" );
fi;
if res[1] then
return rec( ok:= true, value:= res[2] );
else
Expand All @@ -38,7 +45,7 @@ InstallGlobalFunction( CallJuliaFunctionWithCatch,
end );

InstallGlobalFunction( CallJuliaFunctionWithKeywordArguments,
{ julia_obj, args, arec } -> Julia.GAP.kwarg_wrapper( julia_obj,
{ julia_obj, args, kwargs } -> Julia.GAP.kwarg_wrapper( julia_obj,
# non-recursive conversions
GAPToJulia( _JL_Vector_Any, args, false ),
GAPToJulia( _JL_Dict_Any, arec, false ) ) );
GAPToJulia( _JL_Dict_Any, kwargs, false ) ) );
2 changes: 2 additions & 0 deletions pkg/JuliaInterface/tst/utils.tst
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ gap> res.ok;
false
gap> StartsWith( res.value, "DomainError" );
true
gap> CallJuliaFunctionWithCatch( Julia.Base.sqrt, [ 4 ], rec() );
rec( ok := true, value := <Julia: 2.0> )

##
gap> JuliaEvalString(fail);
Expand Down
20 changes: 10 additions & 10 deletions src/packages.jl
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ const DOWNLOAD_HELPER = Ref{Downloads.Downloader}()

function init_packagemanager()
#TODO:
# As soon as PackageManager uses utils' Download function,
# we need not replace code from PackageManager anymore.
# As soon as GAP.jl can rely on a good enough version of PackageManager
# we need not replace `PKGMAN_DownloadURL` anymore.
# (And the function should be renamed.)
res = load("PackageManager")
@assert res
Expand Down Expand Up @@ -59,15 +59,15 @@ function init_packagemanager()
# put the new method in the first position
meths = Globals.Download_Methods
Wrappers.Add(meths, GapObj(r, recursive=true), 1)

# monkey patch PackageManager so that we can disable removal of
# package directories for debugging purposes
orig_PKGMAN_RemoveDir = Globals.PKGMAN_RemoveDir
replace_global!(:PKGMAN_RemoveDir, function(dir)
Globals.ValueOption(GapObj("debug")) == true && return
orig_PKGMAN_RemoveDir(dir)
end)
end

# monkey patch PackageManager so that we can disable removal of
# package directories for debugging purposes
orig_PKGMAN_RemoveDir = Globals.PKGMAN_RemoveDir
replace_global!(:PKGMAN_RemoveDir, function(dir)
Globals.ValueOption(GapObj("debug")) == true && return
orig_PKGMAN_RemoveDir(dir)
end)
end

"""
Expand Down
42 changes: 33 additions & 9 deletions src/utils.jl
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ function _setglobal(M::Module, name::Symbol, val::Any)
end
end

# avoid the deprecated `Core._apply`
_apply(func, args) = func(args...)

"""
get_symbols_in_module(m::Module) :: Vector{Symbol}
Expand All @@ -30,32 +33,55 @@ function get_symbols_in_module(m::Module)
end

"""
call_with_catch(juliafunc, arguments)
call_with_catch(func, args::Vector)
call_with_catch(func, args::Vector, kwargs::Dict{Symbol,T}) where T
Return a tuple `(ok, val)`
where `ok` is either `true`, meaning that calling the function `juliafunc`
with `arguments` returns the value `val`,
where `ok` is either `true`, meaning that calling `func`
with arguments `args` (and optionally with keyword arguments given by
the keys and values of `kwargs`) returns the value `val`,
or `false`, meaning that the function call runs into an error;
in the latter case, `val` is set to the string of the error message.
This function is used on the GAP side.
# Examples
```jldoctest
julia> GAP.call_with_catch(sqrt, 2)
julia> GAP.call_with_catch(sqrt, [2])
(true, 1.4142135623730951)
julia> flag, res = GAP.call_with_catch(sqrt, -2);
julia> flag, res = GAP.call_with_catch(sqrt, [-2]);
julia> flag
false
julia> startswith(res, "DomainError")
true
julia> GAP.call_with_catch(range, [2, 10], Dict(:step => 2))
(true, 2:2:10)
julia> flag, res = GAP.call_with_catch(range, [2, 10], Dict(:step => "a"));
julia> flag
false
julia> startswith(res, "MethodError")
true
```
"""
function call_with_catch(juliafunc, arguments)
function call_with_catch(func, args)
try
res = Core._apply(juliafunc, arguments)
res = func(args...)
return (true, res)
catch e
return (false, string(e))
end
end

function call_with_catch(func, args::Vector, kwargs::Dict{Symbol,T}) where T
try
res = func(args...; [k => kwargs[k] for k in keys(kwargs)]...)
return (true, res)
catch e
return (false, string(e))
Expand All @@ -70,7 +96,6 @@ given by the keys and values of `kwargs`.
This function is used on the GAP side, in calls of Julia functions that
require keyword arguments.
Note that `jl_call` and `Core._apply` do not support keyword arguments.
# Examples
```jldoctest
Expand All @@ -79,7 +104,6 @@ julia> range(2, length = 5, step = 2)
julia> GAP.kwarg_wrapper(range, [2], Dict(:length => 5, :step => 2))
2:2:10
```
"""
function kwarg_wrapper(func, args::Vector{T1}, kwargs::Dict{Symbol,T2}) where {T1,T2}
Expand Down

0 comments on commit e5c5358

Please sign in to comment.