DATASET BY-VALUE in parameter definition syntax

Posted by guilmori on 09-Feb-2012 13:40

OE10.2B04

I'm trying to define a method that returns a copy of an internal dataset.

How come the BY-VALUE token does not compile in a method parameters definition ?

From the help "Parameter definition syntax", it seems to me that this should work.

CLASS class1   :

  DEF TEMP-TABLE tt

      FIELD f1 AS CHAR.   

  DEF DATASET ds FOR tt.

  METHOD PUBLIC VOID GetDSCopy(OUTPUT DATASET ds BY-VALUE):

  END METHOD.

END CLASS.

All Replies

Posted by Tim Kuehn on 09-Feb-2012 14:45

I don't see "by-value" as a legal keyword.

"by-reference" exists, and is used by the caller, not the callee.

Posted by guilmori on 09-Feb-2012 15:34

BY-VALUE

    Specified for an INPUT, OUTPUT, or INPUT-OUTPUT TABLE,  TABLE-HANDLE, DATASET, or DATASET-HANDLE parameter in a called routine, this  option forces the parameter to be passed to the local routine by value, which  overrides any BY-REFERENCE option in the corresponding routine invocation. For  more information on BY-REFERENCE, see the Parameter passing syntax reference entry.

Posted by mopfer on 09-Feb-2012 16:58

The default for the calling program is BY-VALUE, you don't have to specify it.  BY-VALUE is specified for the CALLED program, to override any caller that used BY-REFERENCE.

Posted by guilmori on 10-Feb-2012 07:12

mopfer wrote:

The default for the calling program is BY-VALUE, you don't have to specify it.

Yes, but default is not enough. I want to force the caller to get a copy, so he cannot access the encapsulated dataset.

mopfer wrote:

BY-VALUE is specified for the CALLED program, to override any caller that used BY-REFERENCE.

Yes, that is exactly what my example is trying to do in the original post.

How do you do this in a method ?

Posted by Peter Judge on 10-Feb-2012 07:31

guilmori wrote:

mopfer wrote:

The default for the calling program is BY-VALUE, you don't have to specify it.

Yes, but default is not enough. I want to force the caller to get a copy, so he cannot access the encapsulated dataset.

mopfer wrote:

BY-VALUE is specified for the CALLED program, to override any caller that used BY-REFERENCE.

Yes, that is exactly what my example is trying to do in the original post.

How do you do this in a method ?

OUTPUT DATASET or DATASET-HANDLE will make a deep copy (ie value copy, not reference).

-- peter

Posted by guilmori on 10-Feb-2012 07:39

pjudge wrote:

OUTPUT DATASET or DATASET-HANDLE will make a deep copy (ie value copy, not reference).

The CALLER may use BY-REFERENCE, which I want to prevent.

The CALLER must never be able to access the encapsulated dataset.

Posted by Peter Judge on 10-Feb-2012 07:48

The CALLER may use BY-REFERENCE, which I want to prevent.

The CALLER must never be able to access the encapsulated dataset.

>

Ah, I see now. Add BY-REFERENCE to the keyword forget list? Or is this in some cases only.

Alternatively, always clone the dataset before passing it out; it's easiest using the COPY-DATASET method. Yes, you will incur at least one deep copy this way, and maybe two (clone + output) but this way you're sure that the original dataset is left untouched.

-- peter

Posted by jmls on 10-Feb-2012 07:53

or invoke write-json on the dataset to a longchar, and pass that back

as a parameter

Posted by Peter Judge on 10-Feb-2012 07:58

or invoke write-json on the dataset to a longchar, and pass that back

as a parameter

It'd be interesting to compare the performance of (write-json + rehydrate the PDS) vs. (copy-dataset + output param).

-- peter

Posted by guilmori on 10-Feb-2012 08:08

pjudge wrote:

Ah, I see now. Add BY-REFERENCE to the keyword forget list? Or is this in some cases only.

Hehe , no, in some cases only.

pjudge wrote:

Alternatively, always clone the dataset before passing it out; it's easiest using the COPY-DATASET method. Yes, you will incur at least one deep copy this way, and maybe two (clone + output) but this way you're sure that the original dataset is left untouched.

Yeah, I did try this alternative, but I didn't like the extra deep copy.

Unless of course the caller use BY-REFERENCE, but this is quite confusing, especially on a method named GetDSCopy()...

Posted by Peter Judge on 10-Feb-2012 08:23

Yeah, I did try this alternative, but I didn't like the extra deep

copy.

Unless of course the caller use BY-REFERENCE, but this is quite

confusing, especially on a method named GetDSCopy()...

>

If you don't trust that the developer not to change the contents of the dataset, then maybe cloning is the only way to go. If course, now you have a separate copy of the dataset, you can expose the clone as a handle and avoid the (second) deep copy.

-- peter

Posted by Tim Kuehn on 10-Feb-2012 08:33

guilmori wrote:

BY-VALUE

    Specified for an INPUT, OUTPUT, or INPUT-OUTPUT TABLE,  TABLE-HANDLE, DATASET, or DATASET-HANDLE parameter in a called routine, this  option forces the parameter to be passed to the local routine by value, which  overrides any BY-REFERENCE option in the corresponding routine invocation. For  more information on BY-REFERENCE, see the Parameter passing syntax reference entry.


Well there you go!

Posted by guilmori on 10-Feb-2012 08:37

pjudge wrote:

If you don't trust that the developer not to change the contents of the dataset, then maybe cloning is the only way to go.

I would never trust anyone to not use wrongly exposed write privileges.

pjudge wrote:

If course, now you have a separate copy of the dataset, you can expose the clone as a handle and avoid the (second) deep copy.

I want the caller to read a statically defined dataset.

What about the BY-VALUE option not compiling in the method definition? Is this a documentation bug ? Not supported for method, only for procedure ?

Posted by jmls on 10-Feb-2012 08:38

this extremely rough example shows that the xml version is approx 2.5x slower than a pds copy (65ms vs 25ms)

def temp-table ttCustomer  like Customer.

def temp-table ttOrder     like Order.

def temp-table ttOrderLine like Order-Line.

def dataset pdsData for ttcustomer, ttorder, ttorderline

  data-relation for TTCustomer, TTOrder relation-fields (cust-num,cust-num) nested

  data-relation for TTOrder, TTOrderLine relation-fields (order-num,order-num) nested.

def data-source dsCustomer  for Customer.

def data-source dsOrder     for Order.

def data-source dsOrderLine for Order-Line.

def var lv_Data as longchar no-undo.

def var pdsCopy as handle no-undo.

buffer TTCustomer:attach-data-source (data-source dsCustomer:handle).

buffer TTOrder:attach-data-source (data-source dsOrder:handle).

buffer TTOrderLine:attach-data-source (data-source dsOrderLine:handle).

dataset pdsData:fill ().

dataset pdsData:write-xml("longchar",lv_Data).

dataset pdsData:empty-dataset ().

etime(yes).

dataset pdsData:read-xml("longchar",lv_Data,?,?,?).

message etime view-as alert-box.

etime(yes).

create dataset pdsCopy.

pdsCopy:create-like (dataset pdsData:handle).

pdsCopy:copy-dataset (dataset pdsData:handle).

message etime view-as alert-box.

Posted by Thomas Mercer-Hursh on 10-Feb-2012 11:40

Guillaume, it seems to me that you are tying yourself in knots here over a questionable idea.  It seems like you are trying to protect yourself from malicious programmers ... not a very healthy work environment!   And the cost for this is a performance robbing deep copy and using up twice the memory!

Why not just encapsulate the PDS in an object and pass that?  Provide only allowed methods or, at worst, a secret key or something that opens disallowed methods.  Not only will you avoid passing the DS at all, but you can provide any kind of security or control that you need to fit the situation.

Posted by Admin on 10-Feb-2012 12:34

I might be missing something obviously here but as far that I knew the  only way you can make an output parameter to be passed by reference is  to declare it as reference-only in the caller and specify bind on both  the method definition and on method call... otherwise the output dataset  will be by value (deep-copy) regardless of whether or not by-reference  was used by the caller... at least trying that on an already old 10.1C  shows the datasets are different (check unique-id on them and you'll  see, no changes applied to the returned dataset will affect the one in  the callee).

Posted by guilmori on 10-Feb-2012 13:10

tamhas wrote:

Guillaume, it seems to me that you are tying yourself in knots here over a questionable idea.  It seems like you are trying to protect yourself from malicious programmers ... not a very healthy work environment!   And the cost for this is a performance robbing deep copy and using up twice the memory!

Why not just encapsulate the PDS in an object and pass that?  Provide only allowed methods or, at worst, a secret key or something that opens disallowed methods.  Not only will you avoid passing the DS at all, but you can provide any kind of security or control that you need to fit the situation.

This is exactly what I'm doing, a class holding its data using an internal PDS with all its logic dealing with the PDS.

However, at some point, another class need to iterate over the list of items contained in the first class (readonly access), and perform some other logic for each items.

And I would prefer not to move this "external" logic inside the first class to keep concerns separated since it is unrelated to the first class responsibility.

In .Net, I would expose an IEnumerable and be done.

I thought the simplest way to deal with this in ABL would have been to allow the caller to get a copy of the PDS so he can FOR EACH over it.

Posted by guilmori on 10-Feb-2012 13:31

marianedu wrote:

I might be missing something obviously here but as far that I knew the  only way you can make an output parameter to be passed by reference is  to declare it as reference-only in the caller and specify bind on both  the method definition and on method call... otherwise the output dataset  will be by value (deep-copy) regardless of whether or not by-reference  was used by the caller... at least trying that on an already old 10.1C  shows the datasets are different (check unique-id on them and you'll  see, no changes applied to the returned dataset will affect the one in  the callee).

My error.

You are right, BY-REFERENCE would not expose the encapsulated dataset.

Instead it would "inject" the caller's dataset into the called class, for the duration of the method call.

Thus on a method defined like this, it would always return the dataset that was passed in.

     METHOD PUBLIC VOID GetDSCopy(OUTPUT DATASET ds):

     END METHOD.

However, it is still possible for the caller to use BIND and gain access to the internal dataset:

  myObj:GetDSCopy(OUTPUT DATASET-HANDLE hds BIND)

So basically, I'm looking for a way to define method GetDSCopy() that would:

  - never allow the caller to access internal dataset

  - incur a single deep copy

Posted by Thomas Mercer-Hursh on 10-Feb-2012 13:37

How is iterating over the dataset not a part of the subject matter of basic navigation of the dataset?

Most extreme case, return a new class which is a read-only encapsulated version of the dataset, but the same dataset ... but you are going to have to convince me that makes sense compared to just providing the navigation in the same class.

Posted by Admin on 10-Feb-2012 13:38

guilmori wrote:

     METHOD PUBLIC VOID GetDSCopy(OUTPUT DATASET ds):

     END METHOD.

However, it is still possible for the caller to use BIND and gain access to the internal dataset:

  myObj:GetDSCopy(OUTPUT DATASET-HANDLE hds BIND)

not unless you use bind in your method definition as well, like this...

method public void getDsCopy (output dataset ds bind):

end.

Posted by guilmori on 10-Feb-2012 19:36

marianedu wrote:

not unless you use bind in your method definition as well, like this...

method public void getDsCopy (output dataset ds bind):
end.


Not true. BIND can be specified in the caller only, not required in the called method definition.

Posted by Thomas Mercer-Hursh on 10-Feb-2012 20:13

"can be specified" does not mean that it alone means a pass by reference.

Marian is saying that unless both sides set up for a pass by reference, one gets pass by value, aka a deep copy.  The two are unique to the object.

Of course, I think pass by value is objectionable and you shouldn't be passing it in the first place, but I don't think you actually have a problem here.

Posted by Admin on 11-Feb-2012 00:52

guilmori wrote:

Not true. BIND can be specified in the caller only, not required in the called method definition.

sure it can, it compile just fine but... have you tried that?

if you try to bound to a dataset and the callee doen't allow that (by using bind option in method definition) you should get the following error...

If a caller or called REFERENCE-ONLY parameter

will become BOUND to its opposite as a result of the

call, then the BIND keyword must be supplied on both the caller

invocation and on the called parameter definition. (13161)

Posted by Admin on 11-Feb-2012 01:01

tamhas wrote:

Of course, I think pass by value is objectionable and you shouldn't be passing it in the first place, but I don't think you actually have a problem here.

agreed, something like a collection (in fact an iterator over it) would be preffered and IEnumerable was mentioned before... but this could work for a particular temp-table not the whole dataset

Posted by guilmori on 13-Feb-2012 07:05

marianedu wrote:

guilmori wrote:

Not true. BIND can be specified in the caller only, not required in the called method definition.

sure it can, it compile just fine but... have you tried that?

if you try to bound to a dataset and the callee doen't allow that (by using bind option in method definition) you should get the following error...

If a caller or called REFERENCE-ONLY parameter

will become BOUND to its opposite as a result of the

call, then the BIND keyword must be supplied on both the caller

invocation and on the called parameter definition. (13161)

Hmm.. works fine for me.

What test did you do ?

/* Test.p */

DEF VAR o AS class1.
DEF VAR hds AS HANDLE.

o = NEW Class1().
o:GetDSCopy(OUTPUT DATASET-HANDLE hds bind).

MESSAGE "caller" hds hds:UNIQUE-ID
      VIEW-AS ALERT-BOX.
     
o:DisplayData().

hds:GET-BUFFER-HANDLE(1):FIND-FIRST("").

DO TRANSACTION:
  hds:GET-BUFFER-HANDLE(1):BUFFER-FIELD("f1"):BUFFER-VALUE = "caller changed data".    

END.

o:DisplayData().

/* Class1.cls

CLASS class1:

  DEF TEMP-TABLE tt
      FIELD f1 AS CHAR.   
   
  DEF DATASET ds FOR tt.
 
  CONSTRUCTOR PUBLIC class1 (  ):
    DO TRANSACTION:
      CREATE tt.
      ASSIGN tt.f1 = "callee data".
    END.
     
    MESSAGE "callee constructor" DATASET ds:HANDLE DATASET ds:unique-id
        VIEW-AS ALERT-BOX.
  END CONSTRUCTOR.

  METHOD PUBLIC VOID GetDSCopy(OUTPUT DATASET ds):
    MESSAGE "callee method" DATASET ds:HANDLE DATASET ds:unique-id
        VIEW-AS ALERT-BOX.
  END METHOD.
 
  METHOD PUBLIC VOID DisplayData():
    FOR EACH tt:
        MESSAGE tt.f1 VIEW-AS ALERT-BOX.
    END.
  END METHOD.
   
END CLASS.

Posted by Admin on 13-Feb-2012 07:22

huh, definitively works and imho this is a bug...

the way I've tried was using a dataset parameter not a dataset-handle, in that case you need to define the dataset as reference-only in caller and bind need to be specified on both ends otherwise you get that error... bref, it does look inconsistent and the way you tried does shows it violates the encapsulation

define temp-table tt reference-only

     field f1 as char.

define dataset ds reference-only for tt.

def var o as Class1.

o = new Class1().

o:getDSCopy (output dataset ds bind).

This thread is closed