Repository: scikit-hep/awkward

Issue:
Can `_touch_data` be made public?
### Description of new feature

I have a [use case](https://github.com/bluesky/tiled/pull/549/files#diff-40070db129d1d3432609597f627cdbcddf5f970b7bb8062862427e8501359ab1R32-R34) for calling the [`_touch_data`](https://github.com/search?q=repo%3Ascikit-hep%2Fawkward%20%22def%20_touch_data%22&type=code) methods directly. I wonder if that method can be promoted from a private method to "public but not widely advertised".

If the maintainers are receptive, I would be happy to contribute a PR. It would be easy enough to provide a backward-compatible implementation:

```py
    def touch_data(self):
        ...

    _touch_data = touch_data  # back-compat
```

if that is an important consideration here.


Discussion:
Interestingly, we technically already expose the recursive case at the top level, with 
```python
ak.typetracer.empty_if_typetracer(array)
```

It does some other work besides touching the layout, but it should suffice for your use case, I believe :)
Thanks! Let me see if I can follow how to slot that in.

Current implementation (via @jpivarski):

```py
>>> import awkward as ak
>>> array = ak.Array([[{"x": 1.1, "y": [1]}, {"x": 2.2, "y": [1, 2]}], [], [{"x": 3.3, "y": [1, 2, 3]}]])
>>> form, length, container = ak.to_buffers(array)

>>> typetracer, report = ak.typetracer.typetracer_with_report(form, forget_length=True)
>>> proxy_array = ak.Array(typetracer)
# using _touch_data, internal API
>>> proxy_array[0, "y", 1:].layout._touch_data(recursive=True)
>>> report.data_touched
['node0', 'node3', 'node4']
```

Proposed replacement:

```py
>>> typetracer2, report2 = ak.typetracer.typetracer_with_report(form, forget_length=True)
>>> proxy_array2 = ak.Array(typetracer2)
# empty_if_typetracer is deprecated; its recommended replacement is length_zero_if_typetracer
>>> ak.typetracer.length_zero_if_typetracer(proxy_array2[0, "y", 1:])
<Array [] type='0 * var * int64'>
>>> report2.data_touched
['node0', 'node3', 'node4']
```

So far, so good. What sort of extra work does it do?

Resolving patch:
diff --git a/src/awkward/_do.py b/src/awkward/_do.py
--- a/src/awkward/_do.py
+++ b/src/awkward/_do.py
@@ -423,3 +423,11 @@ def sort(
     starts = ak.index.Index64.zeros(1, nplike=layout.backend.index_nplike)
     parents = ak.index.Index64.zeros(layout.length, nplike=layout.backend.index_nplike)
     return layout._sort_next(negaxis, starts, parents, 1, ascending, stable)
+
+
+def touch_data(layout: Content, recursive: bool = True):
+    layout._touch_data(recursive)
+
+
+def touch_shape(layout: Content, recursive: bool = True):
+    layout._touch_shape(recursive)
diff --git a/src/awkward/typetracer.py b/src/awkward/typetracer.py
--- a/src/awkward/typetracer.py
+++ b/src/awkward/typetracer.py
@@ -8,10 +8,12 @@
     "typetracer_with_report",
     "PlaceholderArray",
     "unknown_length",
+    "touch_data",
 ]
 
 from awkward._backends.typetracer import TypeTracerBackend
 from awkward._behavior import behavior_of
+from awkward._do import touch_data as _touch_data
 from awkward._errors import deprecate
 from awkward._layout import wrap_layout
 from awkward._nplikes.placeholder import PlaceholderArray
@@ -30,20 +32,20 @@
 T = TypeVar("T", Array, Record)
 
 
-def _length_0_1_if_typetracer(array: T, function):
+def _length_0_1_if_typetracer(array, function, highlevel: bool, behavior) -> T:
     typetracer_backend = TypeTracerBackend.instance()
 
     layout = to_layout(array, allow_other=False)
-    behavior = behavior_of(array)
+    behavior = behavior_of(array, behavior=behavior)
 
     if layout.backend is typetracer_backend:
-        layout._touch_data(True)
+        _touch_data(layout)
         layout = function(layout.form, highlevel=False)
 
-    return wrap_layout(layout, behavior=behavior)
+    return wrap_layout(layout, behavior=behavior, highlevel=highlevel)
 
 
-def empty_if_typetracer(array: T) -> T:
+def empty_if_typetracer(array) -> T:
     deprecate(
         "'empty_if_typetracer' is being replaced by 'length_zero_if_typetracer' (change name)",
         "2.4.0",
@@ -52,9 +54,50 @@ def empty_if_typetracer(array: T) -> T:
     return length_zero_if_typetracer(array)
 
 
-def length_zero_if_typetracer(array: T) -> T:
-    return _length_0_1_if_typetracer(array, Form.length_zero_array)
+def length_zero_if_typetracer(array, *, highlevel: bool = True, behavior=None) -> T:
+    """
+    Args:
+        array: Array-like data (anything #ak.to_layout recognizes).
+        highlevel (bool): If True, return an #ak.Array; otherwise, return
+            a low-level #ak.contents.Content subclass.
+        behavior (None or dict): Custom #ak.behavior for the output array, if
+            high-level.
 
+    Recursively touches the data of an array, before returning a length-zero
+    NumPy-backed iff. the given array has a typetracer backend; otherwise, a
+    shallow copy of the original array is returned.
+    """
+    return _length_0_1_if_typetracer(array, Form.length_zero_array, highlevel, behavior)
 
-def length_one_if_typetracer(array: T) -> T:
-    return _length_0_1_if_typetracer(array, Form.length_one_array)
+
+def length_one_if_typetracer(array, *, highlevel: bool = True, behavior=None) -> T:
+    """
+    Args:
+        array: Array-like data (anything #ak.to_layout recognizes).
+        highlevel (bool): If True, return an #ak.Array; otherwise, return
+            a low-level #ak.contents.Content subclass.
+        behavior (None or dict): Custom #ak.behavior for the output array, if
+            high-level.
+
+    Recursively touches the data of an array, before returning a length-one
+    NumPy-backed iff. the given array has a typetracer backend; otherwise, a
+    shallow copy of the original array is returned.
+    """
+    return _length_0_1_if_typetracer(array, Form.length_one_array, highlevel, behavior)
+
+
+def touch_data(array, *, highlevel: bool = True, behavior=None) -> T:
+    """
+    Args:
+        array: Array-like data (anything #ak.to_layout recognizes).
+        highlevel (bool): If True, return an #ak.Array; otherwise, return
+            a low-level #ak.contents.Content subclass.
+        behavior (None or dict): Custom #ak.behavior for the output array, if
+            high-level.
+
+    Recursively touches the data and returns a shall copy of the given array.
+    """
+    behavior = behavior_of(array, behavior=behavior)
+    layout = to_layout(array, allow_other=False)
+    _touch_data(layout)
+    return wrap_layout(layout, behavior=behavior, highlevel=highlevel)


Test patch:
diff --git a/tests/test_2672_touch_data.py b/tests/test_2672_touch_data.py
new file mode 100644
--- /dev/null
+++ b/tests/test_2672_touch_data.py
@@ -0,0 +1,54 @@
+# BSD 3-Clause License; see https://github.com/scikit-hep/awkward-1.0/blob/main/LICENSE
+
+import numpy as np  # noqa: F401
+import pytest  # noqa: F401
+
+import awkward as ak
+
+
+def test():
+    form = ak.forms.from_dict(
+        {
+            "class": "RecordArray",
+            "fields": ["x", "y"],
+            "contents": [
+                {
+                    "class": "ListOffsetArray",
+                    "offsets": "i64",
+                    "content": {
+                        "class": "NumpyArray",
+                        "primitive": "int64",
+                        "inner_shape": [],
+                        "parameters": {},
+                        "form_key": "x.list.content",
+                    },
+                    "parameters": {},
+                    "form_key": "x.list.offsets",
+                },
+                {
+                    "class": "ListOffsetArray",
+                    "offsets": "i64",
+                    "content": {
+                        "class": "NumpyArray",
+                        "primitive": "int64",
+                        "inner_shape": [],
+                        "parameters": {},
+                        "form_key": "y.list.content",
+                    },
+                    "parameters": {},
+                    "form_key": "y.list.offsets",
+                },
+            ],
+            "parameters": {},
+        }
+    )
+    layout, report = ak.typetracer.typetracer_with_report(form)
+    array = ak.Array(layout)
+
+    y = array.y
+    assert len(report.data_touched) == 0
+    assert len(report.shape_touched) == 0
+
+    ak.typetracer.touch_data(y)
+    assert len(report.data_touched) == 2
+    assert len(report.shape_touched) == 2